Skip to content

Commit fc2f2bb

Browse files
authored
Merge pull request #41 from Precise-Finance/feature/engines
Add MLEngine support
2 parents 6e1a228 + f4a6eb1 commit fc2f2bb

11 files changed

+488
-7
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mindsdb-js-sdk",
3-
"version": "2.2.2",
3+
"version": "2.3.0",
44
"author": "MindsDB",
55
"license": "MIT",
66
"description": "Official JavaScript SDK for MindsDB",

src/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export default class Constants {
1717
/** MindsDB Projects endpoint. */
1818
public static readonly BASE_PROJECTS_URI = '/api/projects';
1919

20+
/** MindsDB ML Engines endpoint. */
21+
public static readonly BASE_MLENGINES_URI = '/api/handlers/byom';
22+
23+
2024
// HTTP agent constants.
2125

2226
/** How long to wait for an HTTP response before timeout. */

src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import SqlQueryResult from './sql/sqlQueryResult';
3030
import Table from './tables/table';
3131
import { JsonPrimitive, JsonValue } from './util/json';
3232
import View from './views/view';
33+
import MLEnginesModule from './ml_engines/ml_enginesModule';
3334

3435
const defaultAxiosInstance = createDefaultAxiosInstance();
3536
const httpAuthenticator = new HttpAuthenticator();
@@ -46,6 +47,7 @@ const Projects = new ProjectsModule.ProjectsRestApiClient(
4647
);
4748
const Tables = new TablesModule.TablesRestApiClient(SQL);
4849
const Views = new ViewsModule.ViewsRestApiClient(SQL);
50+
const MLEngines = new MLEnginesModule.MLEnginesRestApiClient(SQL, defaultAxiosInstance, httpAuthenticator);
4951

5052
const getAxiosInstance = function (options: ConnectionOptions): Axios {
5153
const httpClient = options.httpClient || defaultAxiosInstance;
@@ -86,7 +88,7 @@ const connect = async function (options: ConnectionOptions): Promise<void> {
8688
}
8789
};
8890

89-
export default { connect, SQL, Databases, Models, Projects, Tables, Views };
91+
export default { connect, SQL, Databases, Models, Projects, Tables, Views, MLEngines };
9092
export {
9193
ConnectionOptions,
9294
Database,

src/ml_engines/ml_engine.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import MLEngineApiClient from './ml_enginesApiClient';
2+
3+
/**
4+
* Represents a MindsDB mlEngine and all supported operations.
5+
*/
6+
export default class MLEngine {
7+
/** API client to use for executing mlEngine operations. */
8+
mlEnginesApiClient: MLEngineApiClient;
9+
10+
/** Name of the mlEngine. */
11+
name: string;
12+
13+
/** Type of the mlEngine (e.g. project, data). */
14+
handler: string;
15+
16+
/** Engine used to create the mlEngine (e.g. postgres). */
17+
connection_data?: any;
18+
19+
/**
20+
*
21+
* @param {MLEngineApiClient} mlEnginesApiClient - API client to use for executing mlEngine operations.
22+
* @param {string} name - Name of the mlEngine.
23+
* @param {string} type - Type of the mlEngine (e.g. project, data).
24+
* @param {string} connection_data - Engine used to create the mlEngine (e.g. postgres).
25+
*/
26+
constructor(
27+
mlEnginesApiClient: MLEngineApiClient,
28+
name: string,
29+
handler: string,
30+
connection_data: string | undefined
31+
) {
32+
this.mlEnginesApiClient = mlEnginesApiClient;
33+
this.name = name;
34+
this.handler = handler;
35+
this.connection_data = connection_data;
36+
}
37+
38+
/** Deletes this mlEngine.
39+
* @throws {MindsDbError} - Something went wrong deleting the mlEngine.
40+
*/
41+
async delete(): Promise<void> {
42+
await this.mlEnginesApiClient.deleteMLEngine(this.name);
43+
}
44+
}

src/ml_engines/ml_enginesApiClient.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import MLEngine from './ml_engine';
2+
import { Readable } from 'stream';
3+
4+
/**
5+
* Abstract class outlining MLEngine API operations supported by the SDK.
6+
*/
7+
export default abstract class MLEnginesApiClient {
8+
/**
9+
* Gets all mlEngines for the authenticated user.
10+
* @returns {Promise<Array<MLEngine>>} - All mlEngines for the user.
11+
*/
12+
abstract getAllMLEngines(): Promise<Array<MLEngine>>;
13+
14+
/**
15+
* Gets a mlEngine by name for the authenticated user.
16+
* @param {string} name - Name of the mlEngine.
17+
* @returns {Promise<MLEngine | undefined>} - Matching mlEngine, or undefined if it doesn't exist.
18+
*/
19+
abstract getMLEngine(name: string): Promise<MLEngine | undefined>;
20+
21+
/**
22+
* Creates a mlEngine with the given name, engine, and parameters.
23+
* @param {string} name - Name of the mlEngine to be created.
24+
* @param {string | Readable} [codeFilePath] - Path to the code file or Readable of to be used for the mlEngine.
25+
* @param {string | Readable} [modulesFilePath] - Path to the modules file or Readable of to be used for the mlEngine.
26+
* @returns {Promise<MLEngine>} - Newly created mlEngine.
27+
* @throws {MindsDbError} - Something went wrong creating the mlEngine.
28+
*/
29+
abstract createMLEngine(
30+
name: string,
31+
codeFilePath: string | Readable,
32+
modulesFilePath: string | Readable
33+
): Promise<MLEngine | undefined>;
34+
35+
/**
36+
* Updates a mlEngine with the given name, engine, and parameters.
37+
* @param {string} name - Name of the mlEngine to be created.
38+
* @param {string | Readable} [codeFilePath] - Path to the code file to be used for the mlEngine.
39+
* @param {string | Readable} [modulesFilePath] - Path to the modules file to be used for the mlEngine.
40+
* @returns {Promise<MLEngine>} - Newly created mlEngine.
41+
* @throws {MindsDbError} - Something went wrong creating the mlEngine.
42+
*/
43+
abstract updateMLEngine(
44+
name: string,
45+
codeFilePath: string | Readable,
46+
modulesFilePath: string | Readable
47+
): Promise<MLEngine | undefined>;
48+
49+
/**
50+
* Deletes a mlEngine by name.
51+
* @param {string} name - Name of the mlEngine to be deleted.
52+
* @throws {MindsDbError} - Something went wrong deleting the mlEngine.
53+
*/
54+
abstract deleteMLEngine(name: string): Promise<void>;
55+
}

src/ml_engines/ml_enginesModule.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import MLEnginesRestApiClient from './ml_enginesRestApiClient';
2+
3+
export default { MLEnginesRestApiClient: MLEnginesRestApiClient };
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import mysql from 'mysql';
2+
3+
import SqlApiClient from '../sql/sqlApiClient';
4+
import { MindsDbError } from '../errors';
5+
import MLEngine from './ml_engine';
6+
import MLEngineApiClient from './ml_enginesApiClient';
7+
import { Axios } from 'axios';
8+
import HttpAuthenticator from '../httpAuthenticator';
9+
import Constants from '../constants';
10+
import * as fs from 'fs';
11+
import FormData from 'form-data';
12+
import * as path from 'path'; // Import the path module
13+
import { Readable } from 'stream';
14+
import { getBaseRequestConfig } from '../util/http';
15+
16+
/** Implementation of MLEnginesApiClient that goes through the REST API. */
17+
export default class MLEnginesRestApiClient extends MLEngineApiClient {
18+
/** Axios client to send all HTTP requests. */
19+
client: Axios;
20+
21+
/** Authenticator to use for reauthenticating if needed. */
22+
authenticator: HttpAuthenticator;
23+
24+
/** SQL API client to send all SQL query requests. */
25+
sqlClient: SqlApiClient;
26+
27+
/**
28+
*
29+
* @param {SqlApiClient} sqlClient - SQL API client to send all SQL query requests.
30+
*/
31+
constructor(
32+
sqlClient: SqlApiClient,
33+
client: Axios,
34+
authenticator: HttpAuthenticator
35+
) {
36+
super();
37+
this.sqlClient = sqlClient;
38+
this.client = client;
39+
this.authenticator = authenticator;
40+
}
41+
42+
private getMLEnginesUrl(): string {
43+
const baseUrl =
44+
this.client.defaults.baseURL || Constants.BASE_CLOUD_API_ENDPOINT;
45+
const mlEnginesUrl = new URL(Constants.BASE_MLENGINES_URI, baseUrl);
46+
return mlEnginesUrl.toString();
47+
}
48+
49+
/**
50+
* Updates a mlEngine with the given name, engine, and parameters.
51+
* @param {string} name - Name of the MLEngine to be created.
52+
* @param {string | Readable} [codeFilePath] - Path to the code file or Readable of to be used for the mlEngine.
53+
* @param {string | Readable} [modulesFilePath] - Path to the modules file or Readable of to be used for the mlEngine.
54+
* @returns {Promise<MLEngine>} - Newly created mlEngine.
55+
* @throws {MindsDbError} - Something went wrong creating the mlEngine.
56+
*/
57+
override async updateMLEngine(
58+
name: string, // This is the variable that will be used in the URL
59+
codeFilePath: string | Readable,
60+
modulesFilePath: string | Readable
61+
): Promise<MLEngine | undefined> {
62+
return this.createOrUpdateMLEngine(
63+
'post',
64+
name,
65+
codeFilePath,
66+
modulesFilePath
67+
);
68+
}
69+
70+
private async createOrUpdateMLEngine(
71+
httpMethod: 'post' | 'put',
72+
name: string,
73+
codeFilePathOrStream: string | Readable,
74+
modulesFilePathOrStream: string | Readable
75+
): Promise<MLEngine | undefined> {
76+
const formData = new FormData();
77+
78+
formData.append('source', name);
79+
80+
// Append the 'code' file part, checking if it's a stream or a string
81+
if (codeFilePathOrStream instanceof Readable) {
82+
formData.append('code', codeFilePathOrStream, {
83+
filename: 'model.py', // Provide an appropriate default filename or derive from context
84+
contentType: 'text/x-python-script',
85+
});
86+
} else if (typeof codeFilePathOrStream === 'string') {
87+
if (fs.existsSync(codeFilePathOrStream)) {
88+
formData.append('code', fs.createReadStream(codeFilePathOrStream), {
89+
filename: path.basename(codeFilePathOrStream),
90+
contentType: 'text/x-python-script',
91+
});
92+
} else {
93+
console.error('File does not exist:', codeFilePathOrStream);
94+
}
95+
}
96+
97+
// Append the 'modules' file part, checking if it's a stream or a string
98+
if (modulesFilePathOrStream instanceof Readable) {
99+
formData.append('modules', modulesFilePathOrStream, {
100+
filename: 'requirements.txt', // Provide an appropriate default filename or derive from context
101+
contentType: 'text/plain',
102+
});
103+
} else if (typeof modulesFilePathOrStream === 'string') {
104+
if (fs.existsSync(modulesFilePathOrStream)) {
105+
formData.append(
106+
'modules',
107+
fs.createReadStream(modulesFilePathOrStream),
108+
{
109+
filename: path.basename(modulesFilePathOrStream),
110+
contentType: 'text/plain',
111+
}
112+
);
113+
} else {
114+
console.error('File does not exist:', modulesFilePathOrStream);
115+
}
116+
}
117+
118+
// Axios request configuration
119+
const { authenticator, client } = this;
120+
121+
const config = getBaseRequestConfig(authenticator);
122+
const mlEngineUrl = this.getMLEnginesUrl();
123+
config.method = httpMethod;
124+
config.url = `${mlEngineUrl}/${encodeURIComponent(name)}`;
125+
(config.headers = {
126+
...config.headers,
127+
...formData.getHeaders(),
128+
}),
129+
(config.data = formData);
130+
131+
try {
132+
const mlEnginesResponse = await client.request(config);
133+
console.log(JSON.stringify(mlEnginesResponse.data, null, 2));
134+
if (mlEnginesResponse.status === 200) {
135+
return this.getMLEngine(name);
136+
}
137+
return mlEnginesResponse.data;
138+
} catch (error) {
139+
console.error(error);
140+
throw MindsDbError.fromHttpError(error, mlEngineUrl);
141+
}
142+
}
143+
144+
/**
145+
* Creates a mlEngine with the given name, engine, and parameters.
146+
* @param {string} name - Name of the MLEngine to be created.
147+
* @param {string | Readable} [codeFilePath] - Path to the code file or Readable of to be used for the mlEngine.
148+
* @param {string | Readable} [modulesFilePath] - Path to the modules file or Readable of to be used for the mlEngine.
149+
* @returns {Promise<MLEngine>} - Newly created mlEngine.
150+
* @throws {MindsDbError} - Something went wrong creating the mlEngine.
151+
*/
152+
override async createMLEngine(
153+
name: string, // This is the variable that will be used in the URL
154+
codeFilePath: string | Readable,
155+
modulesFilePath: string | Readable
156+
): Promise<MLEngine | undefined> {
157+
return this.createOrUpdateMLEngine(
158+
'put',
159+
name,
160+
codeFilePath,
161+
modulesFilePath
162+
);
163+
}
164+
165+
// Usage example:
166+
// uploadModel('myEngineName', '/path/to/code.py', '/path/to/requirements.txt');
167+
168+
/**
169+
* Gets all mlEngines for the authenticated user.
170+
* @returns {Promise<Array<MLEngine>>} - All mlEngines for the user.
171+
*/
172+
override async getAllMLEngines(): Promise<Array<MLEngine>> {
173+
const showMLEnginesQuery = `SHOW ML_ENGINES`;
174+
const sqlQueryResponse = await this.sqlClient.runQuery(showMLEnginesQuery);
175+
return sqlQueryResponse.rows.map(
176+
(r) =>
177+
new MLEngine(
178+
this,
179+
r['name'],
180+
r['handler'],
181+
this.parseJson(r['connection_data'] as string)
182+
)
183+
);
184+
}
185+
186+
/**
187+
* Gets a mlEngine by name for the authenticated user.
188+
* @param {string} name - Name of the mlEngine.
189+
* @returns {Promise<MLEngine | undefined>} - Matching mlEngine, or undefined if it doesn't exist.
190+
*/
191+
override async getMLEngine(name: string): Promise<MLEngine | undefined> {
192+
const showMLEnginesQuery = `SHOW ML_ENGINES`;
193+
const sqlQueryResponse = await this.sqlClient.runQuery(showMLEnginesQuery);
194+
const mlEngineRow = sqlQueryResponse.rows.find((r) => r['name'] === name);
195+
if (!mlEngineRow) {
196+
return undefined;
197+
}
198+
return new MLEngine(
199+
this,
200+
mlEngineRow['name'],
201+
mlEngineRow['handler'],
202+
this.parseJson(mlEngineRow['connection_data'] as string)
203+
);
204+
}
205+
206+
private parseJson(json?: string): any {
207+
if (!json) {
208+
return undefined;
209+
}
210+
return JSON.parse(
211+
json
212+
?.replace(/'/g, '"')
213+
.replace(/"([^"]+)":\s*"/g, '"$1":"')
214+
.replace(/:\s*"(.*?)"/g, function (match, p1) {
215+
return ': "' + p1.replace(/"/g, '\\"') + '"';
216+
})
217+
);
218+
}
219+
220+
/**
221+
* Deletes a mlEngine by name.
222+
* @param {string} name - Name of the mlEngine to be deleted.
223+
* @throws {MindsDbError} - Something went wrong deleting the mlEngine.
224+
*/
225+
override async deleteMLEngine(name: string): Promise<void> {
226+
const dropMLEngineQuery = `DROP ML_ENGINE ${mysql.escapeId(name)}`;
227+
const sqlQueryResult = await this.sqlClient.runQuery(dropMLEngineQuery);
228+
if (sqlQueryResult.error_message) {
229+
throw new MindsDbError(sqlQueryResult.error_message);
230+
}
231+
}
232+
}

0 commit comments

Comments
 (0)