From 52850e3cf7b5afb6978cb17b47b401b41b32a3fd Mon Sep 17 00:00:00 2001 From: amirnd51 Date: Mon, 22 Jul 2024 18:05:31 +0000 Subject: [PATCH 1/3] [WIP] Python API --- .env | 17 -- .gitignore | 4 + api/db/models/framework.go | 4 + python_api/api.py | 492 ++++++++++++++++++++++++++++++ python_api/db.py | 87 ++++++ python_api/mq.py | 68 +++++ python_api/req.txt | 68 +++++ python_api/schema_mlmodelscope.py | 198 ++++++++++++ 8 files changed, 921 insertions(+), 17 deletions(-) delete mode 100644 .env create mode 100644 python_api/api.py create mode 100644 python_api/db.py create mode 100644 python_api/mq.py create mode 100644 python_api/req.txt create mode 100644 python_api/schema_mlmodelscope.py diff --git a/.env b/.env deleted file mode 100644 index 581ce02..0000000 --- a/.env +++ /dev/null @@ -1,17 +0,0 @@ -DOCKER_REGISTRY=c3sr -ENVIRONMENT=local. -API_VERSION=latest -DB_DRIVER=postgres -DB_HOST=db -DB_PORT=5432 -DB_USER=c3sr -DB_PASSWORD=password -DB_DBNAME=c3sr -MQ_USER=admin -MQ_PASSWORD=password -MQ_HOST=mq -MQ_PORT=5672 -tracer_PORT=4317 -tracer_HOST=trace -MQ_ERLANG_COOKIE=quadruple-chocolate-chunk -TRACER_ADDRESS=trace.local.mlmodelscope.org \ No newline at end of file diff --git a/.gitignore b/.gitignore index 055fd8a..f1a2aad 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ .carml_config.yml *.out .env.companion +*pycache/ +.env* +__pycache__/ +*tmp* \ No newline at end of file diff --git a/api/db/models/framework.go b/api/db/models/framework.go index 9353504..4572b67 100644 --- a/api/db/models/framework.go +++ b/api/db/models/framework.go @@ -12,3 +12,7 @@ type Framework struct { Version string `json:"version"` Architectures []Architecture `json:"architectures"` } +type User struct { + gorm.Model + ID string `gorm:"primaryKey" json:"id"` +} diff --git a/python_api/api.py b/python_api/api.py new file mode 100644 index 0000000..4d90df5 --- /dev/null +++ b/python_api/api.py @@ -0,0 +1,492 @@ + +from fastapi import FastAPI, Depends, HTTPException, Query,Request +from typing import List, Optional +import psycopg2 +import psycopg2.extras +import os +# from pydantic import BaseModel +import uvicorn +import json +import pika +import os +from db import * +from fastapi.responses import JSONResponse +from pydantic import BaseModel, Field +import uuid +from mq import * +import logging + +from typing import Optional +from schema_mlmodelscope import * +from datetime import datetime + +app = FastAPI() +logging.basicConfig(level=logging.INFO) +#enable cors +@app.middleware("http") +async def add_cors_header(request, call_next): + response = await call_next(request) + response.headers['Access-Control-Allow-Origin'] = '*' + return response +@app.exception_handler(Exception) +async def generic_exception_handler(request: Request, exc: Exception): + logging.error(f"An error occurred: {exc}") + return JSONResponse( + status_code=500, + content={"message": "An internal server error occurred."}, + ) + +# route /models +@app.get("/models") +async def get_models( + framework_id: Optional[int] = Query(None), + task: Optional[str] = Query(None), + architecture: Optional[str] = Query(None), + query: Optional[str] = Query(None) +): + # cur + # cur = db.cur(cur_factory=psycopg2.extras.Dictcur) + cur,conn=get_db_cur_con() + sql_query = """ + SELECT models.*, frameworks.name as framework_name, frameworks.version as framework_version, array_agg(architectures.name) as architectures + FROM models + JOIN frameworks ON frameworks.id = models.framework_id + LEFT JOIN architectures ON architectures.framework_id = frameworks.id + WHERE 1=1 + """ + + params = {} + + if framework_id: + sql_query += " AND models.framework_id = %(framework_id)s" + params['framework_id'] = framework_id + + if task: + sql_query += " AND models.output_type = %(task)s" + params['task'] = task + + if architecture: + sql_query += " AND architectures.name = %(architecture)s" + params['architecture'] = architecture + + if query: + wildcard = f"%{query.lower()}%" + sql_query += " AND (LOWER(models.name) LIKE %(wildcard)s OR LOWER(models.description) LIKE %(wildcard)s)" + params['wildcard'] = wildcard + + sql_query += " GROUP BY models.id, frameworks.name, frameworks.version" + + cur.execute(sql_query, params) + rows = cur.fetchall() + + models = [] + for row_dict in rows: + + model = { + "id": row_dict["id"], + "created_at": row_dict["created_at"].isoformat(), + "updated_at": row_dict["updated_at"].isoformat(), + "attributes": { + "Top1": row_dict["attribute_top1"], + "Top5": row_dict["attribute_top5"], + "kind": row_dict["attribute_kind"], + "manifest_author": row_dict["attribute_manifest_author"], + "training_dataset": row_dict["attribute_training_dataset"] + }, + "description": row_dict["description"], + "short_description": row_dict["short_description"], + "model": { + "graph_checksum": row_dict["detail_graph_checksum"], + "graph_path": row_dict["detail_graph_path"], + "weights_checksum": row_dict["detail_weights_checksum"], + "weights_path": row_dict["detail_weights_path"] + }, + "framework": { + "id": row_dict["framework_id"], + "name": row_dict["framework_name"], + "version": row_dict["framework_version"], + "architectures": [{"name": arch} for arch in row_dict["architectures"]] + }, + "input": { + "description": row_dict["input_description"], + "type": row_dict["input_type"] + }, + "license": row_dict["license"], + "name": row_dict["name"], + "output": { + "description": row_dict["output_description"], + "type": row_dict["output_type"] + }, + "url": { + "github": row_dict["url_github"], + "citation": row_dict["url_citation"], + "link1": row_dict["url_link1"], + "link2": row_dict["url_link2"] + }, + "version": row_dict["version"] + } + models.append(model) + + cur.close() + + return {"models": [dict(model) for model in models]} + # return models +@app.get("/models/{model_id}") +async def get_model(model_id: int): + cur,conn=get_db_cur_con() + # get all models from the database as json + query=""" SELECT models.*, frameworks.name as framework_name, frameworks.version as framework_version, array_agg(architectures.name) as architectures + FROM models + JOIN frameworks ON frameworks.id = models.framework_id + LEFT JOIN architectures ON architectures.framework_id = frameworks.id WHERE models.id = %s GROUP BY models.id, frameworks.name, frameworks.version""" + # cur.execute(f"SELECT * FROM models WHERE id={model_id} AND deleted_at IS NULL") + cur.execute(query, (model_id,)) + row_dict = cur.fetchone() + + model = { + "id": row_dict["id"], + "created_at": row_dict["created_at"].isoformat(), + "updated_at": row_dict["updated_at"].isoformat(), + "attributes": { + "Top1": row_dict["attribute_top1"], + "Top5": row_dict["attribute_top5"], + "kind": row_dict["attribute_kind"], + "manifest_author": row_dict["attribute_manifest_author"], + "training_dataset": row_dict["attribute_training_dataset"] + }, + "description": row_dict["description"], + "short_description": row_dict["short_description"], + "model": { + "graph_checksum": row_dict["detail_graph_checksum"], + "graph_path": row_dict["detail_graph_path"], + "weights_checksum": row_dict["detail_weights_checksum"], + "weights_path": row_dict["detail_weights_path"] + }, + "framework": { + "id": row_dict["framework_id"], + "name": row_dict["framework_name"], + "version": row_dict["framework_version"], + "architectures": [{"name": arch} for arch in row_dict["architectures"]] + }, + "input": { + "description": row_dict["input_description"], + "type": row_dict["input_type"] + }, + "license": row_dict["license"], + "name": row_dict["name"], + "output": { + "description": row_dict["output_description"], + "type": row_dict["output_type"] + }, + "url": { + "github": row_dict["url_github"], + "citation": row_dict["url_citation"], + "link1": row_dict["url_link1"], + "link2": row_dict["url_link2"] + }, + "version": row_dict["version"] + } + cur.close() + return{"models": [model]} + +@app.get("/frameworks") +async def get_frameworks(): + cur,conn=get_db_cur_con() + + cur.execute(""" + SELECT f.id as framework_id, f.name as framework_name, f.version, + a.name as architecture_name + FROM frameworks f + LEFT JOIN architectures a ON a.framework_id = f.id + """) + rows = cur.fetchall() + cur.close() + + frameworks_dict = {} + for row in rows: + framework_id = row["framework_id"] + if framework_id not in frameworks_dict: + frameworks_dict[framework_id] = { + 'id': framework_id, + 'name': row["framework_name"], + 'version': row["version"], + 'architectures': [] + } + if row["architecture_name"] is not None: + frameworks_dict[framework_id]['architectures'].append({ + 'name': row["architecture_name"] + }) + + return {"frameworks":[framework for framework in frameworks_dict.values()]} + + +@app.get("/frameworks/{framework_id}") +async def get_framework(framework_id: int): + # get all models from the database as json + cur,conn=get_db_cur_con() + cur.execute(f"SELECT * FROM frameworks WHERE id={framework_id}") + framework = cur.fetchone() + # print(model) + json_framework = Framework(*framework).to_dict() + return{"framework": json_framework} + cur.close() + # return models + +@app.get("/") +async def version(): + return {"version": "0.1.0"} + + +@app.get("/experiments/{experiment_id}") +async def get_experiment(experiment_id: str): + cur,conn=get_db_cur_con() + cur.execute(""" + SELECT e.id AS experiment_id, + t.id AS trial_id, + t.created_at, + t.completed_at, + t.source_trial_id + FROM experiments e + JOIN trials t ON e.id = t.experiment_id + WHERE e.id = %s + """, (experiment_id,)) + + # Fetch all results + rows = cur.fetchall() + + if not rows: + raise Exception(f"No experiment found with ID {experiment_id}") + + # Prepare the response structure + result = { + "id": rows[0]['experiment_id'], + "trials": [], + "user_id": "anonymous" + } + + for row in rows: + trial = { + "id": row['trial_id'], + "created_at": row['created_at'], + "completed_at": row['completed_at'], + "source_trial": row['source_trial_id'] + } + result["trials"].append(trial) + return result + +@app.options("/predict") +async def options_predict(): + return JSONResponse( + content={}, + headers={ + "Allow": "OPTIONS, POST", + "Access-Control-Allow-Methods": "OPTIONS, POST", + "Access-Control-Allow-Headers": "Content-Type", + } + ) + + + +class PredictRequest(BaseModel): + architecture: str + batchSize: int + desiredResultModality: str + gpu: bool + inputs: Optional[List[dict]] = Field(default=None) + # input_url: Optional[str] = Field(default=None) + context: Optional[List[str]] = Field(default=[]) + model: int + traceLevel: str + config : Optional[dict] = Field(default={}) + +@app.post("/predict") +async def predict(request: PredictRequest): + #get the request body + + # data = request.get_json() + # print(data) + print(request) + + architecture = request.architecture + batch_size = request.batchSize + desired_result_modality = request.desiredResultModality + gpu = True + inputs = request.inputs + # input_url = request.input_url + model_id = request.model + trace_level = request.traceLevel + context = request.context + # if input_url and not inputs: + # inputs=[input_url] + has_multi_input=False + if inputs and len(inputs)>1: + has_multi_input=True + config = request.config + print(inputs[0]) + first_input=inputs[0] + + + # xxx + trail= get_trial_by_model_and_input( model_id, first_input["src"]) + print(trail) + print("trial") + if trail and trail[2] is not None: + print(trail[2]) + experiment_id = trail[0] + trial_id = trail[1] + return {"experimentId": experiment_id, "trialId": trial_id, "model_id": model_id, "input_url": inputs[0]} + else: + #create a new trial and generate a new uuid experiment + cur,conn=get_db_cur_con() + + + experiment_id=create_expriement( cur, conn) + + trial_id=create_trial( model_id, experiment_id, cur, conn) + create_trial_inputs(trial_id, inputs, cur, conn) + print(trial_id) + + + model=get_model_by_id(model_id,cur,conn) + framework = get_framework_by_id(model['framework_id'],cur,conn) + + + context={} + queue_name=f"agent-{framework['name']}-amd64".lower() + + + + message= makePredictMessage(architecture, batch_size, desired_result_modality, gpu, inputs,has_multi_input,context,config, model["name"], trace_level, 0, "localhost:6831") + + sendPredictMessage(message,queue_name,trial_id) + return {"experimentId": experiment_id, "trialId": trial_id, "model_id": model["name"]} + + + + # print(trail) + + +@app.delete("/trial/{trial_id}") +async def delete_trial(trial_id: str): + pass + +@app.get("/trial/{trial_id}") +async def get_trial(trial_id: str): + cur,conn=get_db_cur_con() + cur.execute(""" + SELECT t.id AS trial_id, + t.result, + t.completed_at, + ti.url AS input_url, + m.id AS modelId, + m.created_at AS model_created_at, + + m.updated_at AS model_updated_at, + m.attribute_top1 AS top1, + m.attribute_top5 AS top5, + m.attribute_kind AS kind, + m.attribute_manifest_author AS manifest_author, + m.attribute_training_dataset AS training_dataset, + m.description, + m.short_description, + m.detail_graph_checksum AS graph_checksum, + m.detail_graph_path AS graph_path, + m.detail_weights_checksum AS weights_checksum, + m.detail_weights_path AS weights_path, + f.id AS framework_id, + f.name AS framework_name, + f.version AS framework_version, + m.input_description, + m.input_type, + m.license, + m.name AS model_name, + m.output_description, + m.output_type, + m.url_github, + m.url_citation, + m.url_link1, + m.url_link2, + a.name AS architecture_name + FROM trials t + JOIN trial_inputs ti ON t.id = ti.trial_id + + JOIN models m ON t.model_id = m.id + JOIN frameworks f ON m.framework_id = f.id + LEFT JOIN architectures a ON a.framework_id = f.id + WHERE t.id = %s + """, (trial_id,)) + + # Fetch the result + row = cur.fetchone() + + if not row: + raise Exception(f"No trial found with ID {trial_id}") + print(row) + # Prepare the response structure + result = { + "id": row['trial_id'], + "inputs": json.loads(row['input_url']), + "completed_at": row['completed_at'], + "results": + + json.loads(row["result"]) if row["result"] is not None else None + + + , + "model": { + "id": row['modelid'], + "created_at": row['model_created_at'], + "updated_at": row['model_updated_at'], + "attributes": { + "Top1": row['top1'], + "Top5": row['top5'], + "kind": row['kind'], + "manifest_author": row['manifest_author'], + "training_dataset": row['training_dataset'] + }, + "description": row['description'], + "short_description": row['short_description'], + "model": { + "graph_checksum": row['graph_checksum'], + "graph_path": row['graph_path'], + "weights_checksum": row['weights_checksum'], + "weights_path": row['weights_path'] + }, + "framework": { + "id": row['framework_id'], + "name": row['framework_name'], + "version": row['framework_version'], + "architectures": [ + { + "name": row['architecture_name'] + } + ] + }, + "input": { + "description": row['input_description'], + "type": row['input_type'] + }, + "license": row['license'], + "name": row['model_name'], + "output": { + "description": row['output_description'], + "type": row['output_type'] + }, + "url": { + "github": row['url_github'], + "citation": row['url_citation'], + "link1": row['url_link1'], + "link2": row['url_link2'] + }, + "version": "1.0" # Assuming version is always 1.0 for this example + } + } + + return result + + + + + + + diff --git a/python_api/db.py b/python_api/db.py new file mode 100644 index 0000000..7e942ae --- /dev/null +++ b/python_api/db.py @@ -0,0 +1,87 @@ +import psycopg2 +import psycopg2.extras +import os +from datetime import datetime +import uuid +import json +# get environment variables +DB_HOST = os.environ.get('DB_HOST', 'localhost') +DB_PORT = os.environ.get('DB_PORT', '5432') +DB_USER = os.environ.get('DB_USER', 'user') +DB_PASS = os.environ.get('DB_PASS', 'password') +DB_NAME = os.environ.get('DB_NAME', 'db') + +DATABASE_URL = f"postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}" +def get_db_cur_con(cursor_factory=psycopg2.extras.RealDictCursor): + conn = psycopg2.connect(DATABASE_URL) + + cur = conn.cursor(cursor_factory=cursor_factory) + return cur, conn + +def get_model_by_id(model_id, cur, conn): + cur.execute("SELECT name, version, framework_id FROM models WHERE id = %s", (model_id,)) + model = cur.fetchone() + return model +def get_framework_by_id(framework_id, cur, conn): + cur.execute("SELECT name, version FROM frameworks WHERE id = %s", (framework_id,)) + framework = cur.fetchone() + return framework +def close_db_cur_con(cur, conn): + cur.close() + conn.close() + +def create_trial( model_id, experiment_id, cur, conn): + trial_id= str(uuid.uuid4()) + cur.execute("INSERT INTO trials (id,model_id,created_at,updated_at ,experiment_id) VALUES (%s,%s,%s,%s, %s) RETURNING id", (trial_id,model_id, datetime.now(), datetime.now() , experiment_id)) + conn.commit() + return trial_id + +def create_trial_inputs(trial_id, inputs, cur, conn): + cur.execute("INSERT INTO trial_inputs (created_at,updated_at,trial_id, url) VALUES (%s,%s,%s, %s)", ( datetime.now(), datetime.now(),trial_id, json.dumps(inputs))) + conn.commit() + +def create_expriement( cur, conn): + experiment_id= str(uuid.uuid4()) + cur.execute("INSERT INTO experiments (id,created_at,updated_at,user_id) VALUES (%s,%s,%s ,%s)", (experiment_id, datetime.now(), datetime.now(), 'anonymous')) + conn.commit() + return experiment_id + + +def get_trial_by_model_and_input(model_id, input_url): + + input_query = """ + SELECT trial_id + FROM trial_inputs + WHERE url = %s + """ + + # Main query to get trial details + query = f""" + SELECT trials.*, + experiments.*, + models.*, + trial_inputs.* + FROM trials + JOIN experiments ON trials.experiment_id = experiments.id + JOIN models ON trials.model_id = models.id + JOIN trial_inputs ON trials.id = trial_inputs.trial_id + WHERE trials.completed_at IS NOT NULL + AND trials.model_id = %s + AND trials.id IN ({input_query}) + """ + + # Execute the query + cur,conn=get_db_cur_con() + cur.execute(query, ( model_id,input_url)) + + # Fetch the result + trial = cur.fetchone() + + if trial is None: + return None + + print(trial['experiment_id'], trial['trial_id']) + return (trial['experiment_id'], trial['trial_id'],trial["completed_at"]) + + + diff --git a/python_api/mq.py b/python_api/mq.py new file mode 100644 index 0000000..467bd3a --- /dev/null +++ b/python_api/mq.py @@ -0,0 +1,68 @@ +import pika +import os +import json +import uuid +import time + +MQ_HOST = os.environ.get('MQ_HOST', 'localhost') +MQ_PORT = int(os.environ.get('MQ_PORT', '5672')) +MQ_USER = os.environ.get('MQ_USER', 'user') +MQ_PASS = os.environ.get('MQ_PASS', 'password') + +print(MQ_HOST) +print(MQ_PORT) +print(MQ_USER) +print(MQ_PASS) + +def connect(): + parameters = pika.ConnectionParameters( + MQ_HOST, + MQ_PORT, + '/', + pika.PlainCredentials(MQ_USER, MQ_PASS), + heartbeat=60, + blocked_connection_timeout=300 + ) + return pika.BlockingConnection(parameters) + +def makePredictMessage(architecture, batch_size, desired_result_modality, gpu, inputs,has_multi_input,context,config,model_name, trace_level, warmups, tracer_address): + return { + "BatchSize": batch_size, + "DesiredResultModality": desired_result_modality, + "InputFiles": inputs, + "HasMultiInput": has_multi_input, + "Context": context, + "Configuration": config, + "ModelName": model_name, + "NumWarmup": warmups, + "TraceLevel": trace_level, + "TracerAddress": tracer_address, + "UseGpu": gpu + } + +def sendPredictMessage(message, queue_name, correlation_id): + while True: + try: + connection = connect() + channel = connection.channel() + message_bytes = json.dumps(message).encode('utf-8') + channel.basic_publish( + exchange='', + routing_key=queue_name, + body=message_bytes, + properties=pika.BasicProperties( + delivery_mode=2, # make message persistent + correlation_id=correlation_id + ) + ) + # print(" [x] Sent 'Predict Message'") + channel.close() + connection.close() + break + except (pika.exceptions.StreamLostError, pika.exceptions.AMQPConnectionError) as e: + print(f"Connection lost: {e}. Reconnecting in 5 seconds...") + time.sleep(5) + except Exception as e: + print(f"An unexpected error occurred: {e}") + break + diff --git a/python_api/req.txt b/python_api/req.txt new file mode 100644 index 0000000..3bdac9f --- /dev/null +++ b/python_api/req.txt @@ -0,0 +1,68 @@ +annotated-types +anyio +asttokens +certifi +click +comm +debugpy +decorator +dnspython +email_validator +exceptiongroup +executing +fastapi +fastapi-cli +h11 +httpcore +httptools +httpx +idna +importlib_metadata +ipykernel +ipython +jedi +Jinja2 +jupyter_client +jupyter_core +markdown-it-py +MarkupSafe +matplotlib-inline +mdurl +nest_asyncio +orjson +packaging +parso +pexpect +pickleshare +pika +platformdirs +prompt_toolkit +psutil +psycopg2-binary +ptyprocess +pure-eval +pydantic +pydantic_core +Pygments +python-dateutil +python-dotenv +python-multipart +PyYAML +pyzmq +rich +shellingham +six +sniffio +stack-data +starlette +tornado +traitlets +typer +typing_extensions +ujson +uvicorn +uvloop +watchfiles +wcwidth +websockets +zipp diff --git a/python_api/schema_mlmodelscope.py b/python_api/schema_mlmodelscope.py new file mode 100644 index 0000000..544501d --- /dev/null +++ b/python_api/schema_mlmodelscope.py @@ -0,0 +1,198 @@ +import datetime +import json +from typing import Optional, List + +class Migration: + def __init__(self, migration: int, migrated_at: datetime.datetime): + self.migration = migration + self.migrated_at = migrated_at + def to_json(self) -> str: + return json.dumps(self.__dict__, indent=4, default=str) + +class Experiment: + def __init__(self, id: str, created_at: datetime.datetime, updated_at: datetime.datetime, + deleted_at: Optional[datetime.datetime], user_id: str): + self.id = id + self.created_at = created_at + self.updated_at = updated_at + self.deleted_at = deleted_at + self.user_id = user_id + def to_json(self) -> str: + return json.dumps(self.__dict__, indent=4, default=str) + def to_dict(self) -> dict: + return self.__dict__ + +class TrialInput: + def __init__(self, id: int, created_at: datetime.datetime, updated_at: datetime.datetime, + deleted_at: Optional[datetime.datetime], trial_id: str, url: str, user_id: str): + self.id = id + self.created_at = created_at + self.updated_at = updated_at + self.deleted_at = deleted_at + self.trial_id = trial_id + self.url = url + self.user_id = user_id + def to_json(self) -> str: + return json.dumps(self.__dict__, indent=4, default=str) + def to_dict(self) -> dict: + return self.__dict__ + +class Trial: + def __init__(self, id: str, created_at: datetime.datetime, updated_at: datetime.datetime, + deleted_at: Optional[datetime.datetime], model_id: int, completed_at: Optional[datetime.datetime], + result: str, experiment_id: str, source_trial_id: Optional[str]): + self.id = id + self.created_at = created_at + self.updated_at = updated_at + self.deleted_at = deleted_at + self.model_id = model_id + self.completed_at = completed_at + self.result = result + self.experiment_id = experiment_id + self.source_trial_id = source_trial_id + def to_json(self) -> str: + return json.dumps(self.__dict__, indent=4, default=str) + def to_dict(self) -> dict: + return self.__dict__ + +class Architecture: + def __init__(self, id: int, created_at: datetime.datetime, updated_at: datetime.datetime, + deleted_at: Optional[datetime.datetime], name: str, framework_id: int): + self.id = id + self.created_at = created_at + self.updated_at = updated_at + self.deleted_at = deleted_at + self.name = name + self.framework_id = framework_id + def to_json(self) -> str: + return json.dumps(self.__dict__, indent=4, default=str) + def to_dict(self) -> dict: + return self.__dict__ + +class Framework: + def __init__(self, id: int, created_at: datetime.datetime, updated_at: datetime.datetime, + deleted_at: Optional[datetime.datetime], name: str, version: str): + self.id = id + self.created_at = created_at + self.updated_at = updated_at + self.deleted_at = deleted_at + self.name = name + self.version = version + def to_json(self) -> str: + return json.dumps(self.__dict__, indent=4, default=str) + def to_dict(self) -> dict: + return self.__dict__ + + + +class User: + def __init__(self, id: str, created_at: datetime.datetime, updated_at: datetime.datetime, + deleted_at: Optional[datetime.datetime]): + self.id = id + self.created_at = created_at + self.updated_at = updated_at + self.deleted_at = deleted_at + def to_json(self) -> str: + #return as a json object + # return + return json.dumps(self.__dict__, indent=4, default=str) + def to_dict(self) -> dict: + return self.__dict__ + + +class Model: + """ + Represents a machine learning model. + + Attributes: + id (int): The ID of the model. + created_at (datetime.datetime): The timestamp when the model was created. + updated_at (datetime.datetime): The timestamp when the model was last updated. + deleted_at (Optional[datetime.datetime]): The timestamp when the model was deleted (if applicable). + attribute_top1 (Optional[str]): The top1 attribute of the model. + attribute_top5 (Optional[str]): The top5 attribute of the model. + attribute_kind (str): The kind attribute of the model. + attribute_manifest_author (str): The manifest author attribute of the model. + attribute_training_dataset (str): The training dataset attribute of the model. + description (str): The description of the model. + detail_graph_checksum (str): The checksum of the model's graph. + detail_graph_path (str): The path to the model's graph. + detail_weights_checksum (str): The checksum of the model's weights. + detail_weights_path (str): The path to the model's weights. + framework_id (Framework): The ID of the framework used by the model. + input_description (str): The description of the model's input. + input_type (str): The type of the model's input. + license (str): The license of the model. + name (str): The name of the model. + output_description (str): The description of the model's output. + output_type (str): The type of the model's output. + version (str): The version of the model. + short_description (str): The short description of the model. + url_github (str): The GitHub URL of the model. + url_citation (str): The citation URL of the model. + url_link1 (str): The first additional URL of the model. + url_link2 (str): The second additional URL of the model. + """ + + def __init__(self, id: int, created_at: datetime.datetime, updated_at: datetime.datetime, + deleted_at: Optional[datetime.datetime], attribute_top1: Optional[str], attribute_top5: Optional[str], + attribute_kind: str, attribute_manifest_author: str, attribute_training_dataset: str, description: str, + detail_graph_checksum: str, detail_graph_path: str, detail_weights_checksum: str, detail_weights_path: str, + framework_id: Framework, input_description: str, input_type: str, license: str, name: str, + output_description: str, output_type: str, version: str, short_description: str, + url_github: str, url_citation: str, url_link1: str, url_link2: str): + self.id = id + self.created_at = created_at + self.updated_at = updated_at + self.deleted_at = deleted_at + self.attributes = { + "Top1": attribute_top1, + "Top5": attribute_top5, + "kind": attribute_kind, + "manifest_author": attribute_manifest_author, + "training_dataset": attribute_training_dataset + } + self.description = description + self.short_description = short_description + self.model = { + "graph_checksum": detail_graph_checksum, + "graph_path": detail_graph_path, + "weights_checksum": detail_weights_checksum, + "weights_path": detail_weights_path + } + self.framework_id = framework_id + self.input = { + "description": input_description, + "type": input_type + } + self.license = license + self.name = name + self.output = { + "description": output_description, + "type": output_type + } + self.url = { + "github": url_github, + "citation": url_citation, + "link1": url_link1, + "link2": url_link2 + } + self.version = version + + def to_json(self) -> str: + """ + Convert the model object to a JSON string. + + Returns: + str: The JSON representation of the model object. + """ + return json.dumps(self.__dict__, default=str) + + def to_dict(self) -> dict: + """ + Convert the model object to a dictionary. + + Returns: + dict: The dictionary representation of the model object. + """ + return self.__dict__ From a7e1e178004a2fc64f91d8ca9f22cd741bfc4f20 Mon Sep 17 00:00:00 2001 From: amirnd51 Date: Mon, 22 Jul 2024 18:07:21 +0000 Subject: [PATCH 2/3] chore: Update README.md --- python_api/README.MD | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 python_api/README.MD diff --git a/python_api/README.MD b/python_api/README.MD new file mode 100644 index 0000000..7a884b3 --- /dev/null +++ b/python_api/README.MD @@ -0,0 +1,6 @@ +# Installation +```pip install -r req.txt``` + +# Running + +```fastapi run api.py --reload``` \ No newline at end of file From 7509b1bc1e52fd8fc8a0d3e6762af16410d34531 Mon Sep 17 00:00:00 2001 From: amirnd51 Date: Wed, 24 Jul 2024 01:00:14 +0000 Subject: [PATCH 3/3] chore: Update Dockerfile for Python API and add CORS middleware --- .github/workflows/build-and-push.yaml | 2 +- docker/Dockerfile.python_api | 21 +++++++++++++++++++++ python_api/Dockerfile | 20 ++++++++++++++++++++ python_api/api.py | 23 ++++++++++++++++------- python_api/mq.py | 5 +---- python_api/{req.txt => requirements.txt} | 0 6 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 docker/Dockerfile.python_api create mode 100644 python_api/Dockerfile rename python_api/{req.txt => requirements.txt} (100%) diff --git a/.github/workflows/build-and-push.yaml b/.github/workflows/build-and-push.yaml index 31f9a64..5389712 100644 --- a/.github/workflows/build-and-push.yaml +++ b/.github/workflows/build-and-push.yaml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v3 - name: Build Image - run: ./scripts/build-container.sh api ${IMAGE_TAGS} ${IMAGE_REGISTRY} + run: ./scripts/build-container.sh python_api ${IMAGE_TAGS} ${IMAGE_REGISTRY} - name: Push to GHCR id: push-to-ghcr diff --git a/docker/Dockerfile.python_api b/docker/Dockerfile.python_api new file mode 100644 index 0000000..e8a4e33 --- /dev/null +++ b/docker/Dockerfile.python_api @@ -0,0 +1,21 @@ +# Use the official Python image from the Docker Hub +FROM python:3.11-slim + +# Set the working directory in the container +WORKDIR /app + +# Copy the requirements.txt file into the container +COPY requirements.txt . + +# Install the dependencies specified in the requirements.txt file +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application code into the container +COPY . . + +# Expose the port that the FastAPI app runs on +EXPOSE 8000 + +# Command to run the FastAPI application with --reload for development +CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"] + diff --git a/python_api/Dockerfile b/python_api/Dockerfile new file mode 100644 index 0000000..0175942 --- /dev/null +++ b/python_api/Dockerfile @@ -0,0 +1,20 @@ +# Use the official Python image from the Docker Hub +FROM python:3.11-slim + +# Set the working directory in the container +WORKDIR /app + +# Copy the requirements.txt file into the container +COPY python_api/requirements.txt . + +# Install the dependencies specified in the requirements.txt file +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application code into the container +COPY python_api/ . + +# Expose the port that the FastAPI app runs on +EXPOSE 8000 + +# Command to run the FastAPI application with --reload for development +CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/python_api/api.py b/python_api/api.py index 4d90df5..0067e3b 100644 --- a/python_api/api.py +++ b/python_api/api.py @@ -19,6 +19,7 @@ from typing import Optional from schema_mlmodelscope import * from datetime import datetime +from fastapi.middleware.cors import CORSMiddleware app = FastAPI() logging.basicConfig(level=logging.INFO) @@ -28,6 +29,14 @@ async def add_cors_header(request, call_next): response = await call_next(request) response.headers['Access-Control-Allow-Origin'] = '*' return response + + # enable cors + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], + ) @app.exception_handler(Exception) async def generic_exception_handler(request: Request, exc: Exception): logging.error(f"An error occurred: {exc}") @@ -305,7 +314,7 @@ async def predict(request: PredictRequest): # data = request.get_json() # print(data) - print(request) + # print(request) architecture = request.architecture batch_size = request.batchSize @@ -322,16 +331,16 @@ async def predict(request: PredictRequest): if inputs and len(inputs)>1: has_multi_input=True config = request.config - print(inputs[0]) + # print(inputs[0]) first_input=inputs[0] # xxx trail= get_trial_by_model_and_input( model_id, first_input["src"]) - print(trail) - print("trial") + # print(trail) + # print("trial") if trail and trail[2] is not None: - print(trail[2]) + # print(trail[2]) experiment_id = trail[0] trial_id = trail[1] return {"experimentId": experiment_id, "trialId": trial_id, "model_id": model_id, "input_url": inputs[0]} @@ -344,7 +353,7 @@ async def predict(request: PredictRequest): trial_id=create_trial( model_id, experiment_id, cur, conn) create_trial_inputs(trial_id, inputs, cur, conn) - print(trial_id) + # print(trial_id) model=get_model_by_id(model_id,cur,conn) @@ -421,7 +430,7 @@ async def get_trial(trial_id: str): if not row: raise Exception(f"No trial found with ID {trial_id}") - print(row) + # print(row) # Prepare the response structure result = { "id": row['trial_id'], diff --git a/python_api/mq.py b/python_api/mq.py index 467bd3a..3b2b8f2 100644 --- a/python_api/mq.py +++ b/python_api/mq.py @@ -9,10 +9,7 @@ MQ_USER = os.environ.get('MQ_USER', 'user') MQ_PASS = os.environ.get('MQ_PASS', 'password') -print(MQ_HOST) -print(MQ_PORT) -print(MQ_USER) -print(MQ_PASS) + def connect(): parameters = pika.ConnectionParameters( diff --git a/python_api/req.txt b/python_api/requirements.txt similarity index 100% rename from python_api/req.txt rename to python_api/requirements.txt