Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MLEngine support #41

Merged
merged 8 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mindsdb-js-sdk",
"version": "2.2.2",
"version": "2.3.0",
"author": "MindsDB",
"license": "MIT",
"description": "Official JavaScript SDK for MindsDB",
Expand Down
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export default class Constants {
/** MindsDB Projects endpoint. */
public static readonly BASE_PROJECTS_URI = '/api/projects';

/** MindsDB ML Engines endpoint. */
public static readonly BASE_MLENGINES_URI = '/api/handlers/byom';


// HTTP agent constants.

/** How long to wait for an HTTP response before timeout. */
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import SqlQueryResult from './sql/sqlQueryResult';
import Table from './tables/table';
import { JsonPrimitive, JsonValue } from './util/json';
import View from './views/view';
import MLEnginesModule from './ml_engines/ml_enginesModule';

const defaultAxiosInstance = createDefaultAxiosInstance();
const httpAuthenticator = new HttpAuthenticator();
Expand All @@ -46,6 +47,7 @@ const Projects = new ProjectsModule.ProjectsRestApiClient(
);
const Tables = new TablesModule.TablesRestApiClient(SQL);
const Views = new ViewsModule.ViewsRestApiClient(SQL);
const MLEngines = new MLEnginesModule.MLEnginesRestApiClient(SQL, defaultAxiosInstance, httpAuthenticator);

const getAxiosInstance = function (options: ConnectionOptions): Axios {
const httpClient = options.httpClient || defaultAxiosInstance;
Expand Down Expand Up @@ -86,7 +88,7 @@ const connect = async function (options: ConnectionOptions): Promise<void> {
}
};

export default { connect, SQL, Databases, Models, Projects, Tables, Views };
export default { connect, SQL, Databases, Models, Projects, Tables, Views, MLEngines };
export {
ConnectionOptions,
Database,
Expand Down
44 changes: 44 additions & 0 deletions src/ml_engines/ml_engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import MLEngineApiClient from './ml_enginesApiClient';

/**
* Represents a MindsDB mlEngine and all supported operations.
*/
export default class MLEngine {
/** API client to use for executing mlEngine operations. */
mlEnginesApiClient: MLEngineApiClient;

/** Name of the mlEngine. */
name: string;

/** Type of the mlEngine (e.g. project, data). */
handler: string;

/** Engine used to create the mlEngine (e.g. postgres). */
connection_data: string | undefined;

/**
*
* @param {MLEngineApiClient} mlEnginesApiClient - API client to use for executing mlEngine operations.
* @param {string} name - Name of the mlEngine.
* @param {string} type - Type of the mlEngine (e.g. project, data).
* @param {string} connection_data - Engine used to create the mlEngine (e.g. postgres).
*/
constructor(
mlEnginesApiClient: MLEngineApiClient,
name: string,
handler: string,
connection_data: string | undefined
) {
this.mlEnginesApiClient = mlEnginesApiClient;
this.name = name;
this.handler = handler;
this.connection_data = connection_data;
}

/** Deletes this mlEngine.
* @throws {MindsDbError} - Something went wrong deleting the mlEngine.
*/
async delete(): Promise<void> {
await this.mlEnginesApiClient.deleteMLEngine(this.name);
}
}
41 changes: 41 additions & 0 deletions src/ml_engines/ml_enginesApiClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { JsonValue } from '../util/json';
import MLEngine from './ml_engine';

/**
* Abstract class outlining MLEngine API operations supported by the SDK.
*/
export default abstract class MLEnginesApiClient {
/**
* Gets all mlEngines for the authenticated user.
* @returns {Promise<Array<MLEngine>>} - All mlEngines for the user.
*/
abstract getAllMLEngines(): Promise<Array<MLEngine>>;

/**
* Gets a mlEngine by name for the authenticated user.
* @param {string} name - Name of the mlEngine.
* @returns {Promise<MLEngine | undefined>} - Matching mlEngine, or undefined if it doesn't exist.
*/
abstract getMLEngine(name: string): Promise<MLEngine | undefined>;

/**
* Creates a mlEngine with the given name, engine, and parameters.
* @param {string} name - Name of the mlEngine to be created.
* @param {string} [codeFilePath] - Path to the code file to be used for the mlEngine.
* @param {string} [modulesFilePath] - Path to the modules file to be used for the mlEngine.
* @returns {Promise<MLEngine>} - Newly created mlEngine.
* @throws {MindsDbError} - Something went wrong creating the mlEngine.
*/
abstract createMLEngine(
name: string,
codeFilePath: string,
modulesFilePath: string
): Promise<MLEngine | undefined>;

/**
* Deletes a mlEngine by name.
* @param {string} name - Name of the mlEngine to be deleted.
* @throws {MindsDbError} - Something went wrong deleting the mlEngine.
*/
abstract deleteMLEngine(name: string): Promise<void>;
}
3 changes: 3 additions & 0 deletions src/ml_engines/ml_enginesModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import MLEnginesRestApiClient from './ml_enginesRestApiClient';

export default { MLEnginesRestApiClient: MLEnginesRestApiClient };
203 changes: 203 additions & 0 deletions src/ml_engines/ml_enginesRestApiClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import mysql from 'mysql';

import SqlApiClient from '../sql/sqlApiClient';
import { MindsDbError } from '../errors';
import MLEngine from './ml_engine';
import MLEngineApiClient from './ml_enginesApiClient';
import { Axios } from 'axios';
import HttpAuthenticator from '../httpAuthenticator';
import Constants from '../constants';
import * as fs from 'fs';
import FormData from 'form-data';
import * as path from 'path'; // Import the path module
import { getBaseRequestConfig } from '../util/http';

/** Implementation of MLEnginesApiClient that goes through the REST API. */
export default class MLEnginesRestApiClient extends MLEngineApiClient {
/** Axios client to send all HTTP requests. */
client: Axios;

/** Authenticator to use for reauthenticating if needed. */
authenticator: HttpAuthenticator;

/** SQL API client to send all SQL query requests. */
sqlClient: SqlApiClient;

/**
*
* @param {SqlApiClient} sqlClient - SQL API client to send all SQL query requests.
*/
constructor(
sqlClient: SqlApiClient,
client: Axios,
authenticator: HttpAuthenticator
) {
super();
this.sqlClient = sqlClient;
this.client = client;
this.authenticator = authenticator;
}

private getMLEnginesUrl(): string {
const baseUrl =
this.client.defaults.baseURL || Constants.BASE_CLOUD_API_ENDPOINT;
const mlEnginesUrl = new URL(Constants.BASE_MLENGINES_URI, baseUrl);
return mlEnginesUrl.toString();
}

/**
* Creates a mlEngine with the given name, engine, and parameters.
* @param {string} name - Name of the MLEngine to be created.
* @param {string} [codeFilePath] - Path to the code file ( path.join(__dirname, 'model.py'))
* @param {string} [modulesFilePath] - Path to the modules file ( path.join(__dirname, 'requirements.txt'))
* @returns {Promise<MLEngine>} - Newly created mlEngine.
* @throws {MindsDbError} - Something went wrong creating the mlEngine.
*/
override async createMLEngine(
name: string, // This is the variable that will be used in the URL
codeFilePath: string,
modulesFilePath: string
): Promise<MLEngine | undefined> {
// Create form data
const formData = new FormData();

// Append the 'source' part
formData.append('source', name);

// Append the 'code' file part
if (fs.existsSync(codeFilePath)) {
// File exists, proceed with your operation
formData.append('code', fs.createReadStream(codeFilePath), {
filename: path.basename(codeFilePath), // The actual name of the file being read
contentType: 'text/x-python-script',
});
} else {
console.error('File does not exist:', codeFilePath);
}

// Append the 'modules' file part
if (fs.existsSync(modulesFilePath)) {
// File exists, proceed with your operation
formData.append('modules', fs.createReadStream(modulesFilePath), {
filename: path.basename(modulesFilePath), // The actual name of the file being read
contentType: 'text/plain',
});
} else {
console.error('File does not exist:', modulesFilePath);
}

// Axios request configuration
const { authenticator, client } = this;

const config = getBaseRequestConfig(authenticator);
const mlEngineUrl = this.getMLEnginesUrl();
config.method = 'put';
config.url = `${mlEngineUrl}/${encodeURIComponent(name)}`;
(config.headers = {
...config.headers,
...formData.getHeaders(),
}),
(config.data = formData);

try {
const mlEnginesResponse = await client.request(config);
console.log(JSON.stringify(mlEnginesResponse.data, null, 2));
if (mlEnginesResponse.status === 200) {
return this.getMLEngine(name);
}
return mlEnginesResponse.data;
} catch (error) {
console.error(error);
throw MindsDbError.fromHttpError(error, mlEngineUrl);
}
}

// Usage example:
// uploadModel('myEngineName', '/path/to/code.py', '/path/to/requirements.txt');

/**
* Gets all mlEngines for the authenticated user.
* @returns {Promise<Array<MLEngine>>} - All mlEngines for the user.
*/
override async getAllMLEngines(): Promise<Array<MLEngine>> {
const showMLEnginesQuery = `SHOW ML_ENGINES`;
const sqlQueryResponse = await this.sqlClient.runQuery(showMLEnginesQuery);
return sqlQueryResponse.rows.map(
(r) => new MLEngine(this, r['name'], r['handler'], r['connection_data'])
);
}

/**
* Gets a mlEngine by name for the authenticated user.
* @param {string} name - Name of the mlEngine.
* @returns {Promise<MLEngine | undefined>} - Matching mlEngine, or undefined if it doesn't exist.
*/
override async getMLEngine(name: string): Promise<MLEngine | undefined> {
const showMLEnginesQuery = `SHOW ML_ENGINES`;
const sqlQueryResponse = await this.sqlClient.runQuery(showMLEnginesQuery);
const mlEngineRow = sqlQueryResponse.rows.find(
(r) => r['name'] === name
);
if (!mlEngineRow) {
return undefined;
}
return new MLEngine(
this,
mlEngineRow['name'],
mlEngineRow['handler'],
mlEngineRow['connection_data']
);
}

// /**
// * Creates a mlEngine with the given name, engine, and parameters.
// * @param {string} name - Name of the mlEngine to be created.
// * @param {string} [engine] - Optional name of the mlEngine engine.
// * @param {string} [params] - Optional parameters used to connect to the mlEngine (e.g. user, password).
// * @returns {Promise<MLEngine>} - Newly created mlEngine.
// * @throws {MindsDbError} - Something went wrong creating the mlEngine.
// */
// override async createMLEngine(
danshapir marked this conversation as resolved.
Show resolved Hide resolved
// name: string,
// engine?: string,
// params?: Record<string, JsonValue>
// ): Promise<MLEngine> {
// // Can't use backtick quotes with CREATE DATABASE since it will be included
// // in the information schema, but we still want to escape the name.
// const escapedName = mysql.escapeId(name);
// const escapedNameNoBackticks = escapedName.slice(1, escapedName.length - 1);
// const createClause = `CREATE DATABASE ${escapedNameNoBackticks}`;
// let engineClause = '';
// let type = 'project';
// if (engine) {
// engineClause = `WITH ENGINE = ${mysql.escape(engine)}`;
// type = 'data';
// }
// let paramsClause = '';
// if (params) {
// engineClause += ',';
// paramsClause = `PARAMETERS = ${JSON.stringify(params)}`;
// }
// const createMLEngineQuery = [createClause, engineClause, paramsClause].join(
// '\n'
// );
// const sqlQueryResult = await this.sqlClient.runQuery(createMLEngineQuery);
// if (sqlQueryResult.error_message) {
// throw new MindsDbError(sqlQueryResult.error_message);
// }
// return new MLEngine(this, name, type, engine);
// }

/**
* Deletes a mlEngine by name.
* @param {string} name - Name of the mlEngine to be deleted.
* @throws {MindsDbError} - Something went wrong deleting the mlEngine.
*/
override async deleteMLEngine(name: string): Promise<void> {
const dropMLEngineQuery = `DROP ML_ENGINE ${mysql.escapeId(name)}`;
const sqlQueryResult = await this.sqlClient.runQuery(dropMLEngineQuery);
if (sqlQueryResult.error_message) {
throw new MindsDbError(sqlQueryResult.error_message);
}
}
}
Loading