Skip to content

Commit 414c9f2

Browse files
authored
chore(native): Support python execution in a separate scope (#6899)
1 parent 27b2977 commit 414c9f2

File tree

8 files changed

+214
-68
lines changed

8 files changed

+214
-68
lines changed

.github/workflows/rust-cubesql.yml

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ jobs:
3131
toolchain: nightly-2022-03-22
3232
override: true
3333
components: rustfmt, clippy
34-
- uses: Swatinem/rust-cache@v1
34+
- uses: Swatinem/rust-cache@v2
3535
with:
36-
working-directory: ./rust/cubesql
36+
workspaces: ./rust/cubesql -> target
3737
# default key
3838
key: cubesql-${{ runner.OS }}-x86_64-unknown-linux-gnu-16
39-
sharedKey: cubesql-${{ runner.OS }}-x86_64-unknown-linux-gnu-16
39+
shared-key: cubesql-${{ runner.OS }}-x86_64-unknown-linux-gnu-16
4040
- name: Lint CubeSQL
4141
run: cd rust/cubesql/cubesql && cargo fmt --all -- --check
4242
- name: Lint Native
@@ -64,12 +64,12 @@ jobs:
6464
toolchain: nightly-2022-03-22
6565
override: true
6666
components: rustfmt
67-
- uses: Swatinem/rust-cache@v1
67+
- uses: Swatinem/rust-cache@v2
6868
with:
69-
working-directory: ./rust/cubesql
69+
workspaces: ./rust/cubesql -> target
7070
# default key
7171
key: cubesql-${{ runner.OS }}-x86_64-unknown-linux-gnu-16
72-
sharedKey: cubesql-${{ runner.OS }}-x86_64-unknown-linux-gnu-16
72+
shared-key: cubesql-${{ runner.OS }}-x86_64-unknown-linux-gnu-16
7373
- name: Install [email protected]
7474
uses: baptiste0928/cargo-install@v1
7575
with:
@@ -107,12 +107,12 @@ jobs:
107107
toolchain: nightly-2022-03-22
108108
override: true
109109
components: rustfmt
110-
- uses: Swatinem/rust-cache@v1
110+
- uses: Swatinem/rust-cache@v2
111111
with:
112-
working-directory: ./rust/cubesql
112+
workspaces: ./rust/cubesql -> target
113113
# default key
114114
key: cubesql-${{ runner.OS }}-x86_64-unknown-linux-gnu-16
115-
sharedKey: cubesql-${{ runner.OS }}-x86_64-unknown-linux-gnu-16
115+
shared-key: cubesql-${{ runner.OS }}-x86_64-unknown-linux-gnu-16
116116
- name: Unit tests (Legacy Engine)
117117
env:
118118
CUBESQL_TESTING_CUBE_TOKEN: ${{ secrets.CUBESQL_TESTING_CUBE_TOKEN }}
@@ -151,11 +151,11 @@ jobs:
151151
override: true
152152
components: rustfmt
153153
target: ${{ matrix.target }}
154-
- uses: Swatinem/rust-cache@v1
154+
- uses: Swatinem/rust-cache@v2
155155
with:
156-
working-directory: ./rust/cubesql
156+
workspaces: ./rust/cubesql -> target
157157
key: cubesql-${{ runner.OS }}-${{ matrix.target }}-${{ matrix.node-version }}
158-
sharedKey: cubesql-${{ runner.OS }}-${{ matrix.target }}-${{ matrix.node-version }}
158+
shared-key: cubesql-${{ runner.OS }}-${{ matrix.target }}-${{ matrix.node-version }}
159159
- name: Install Node.js ${{ matrix.node-version }}
160160
uses: actions/setup-node@v3
161161
with:
@@ -190,24 +190,28 @@ jobs:
190190
if: (matrix.python-version == 'fallback')
191191
env:
192192
CARGO_BUILD_TARGET: ${{ matrix.target }}
193-
run: cd packages/cubejs-backend-native && yarn run native:build-debug
193+
working-directory: ./packages/cubejs-backend-native
194+
run: yarn run native:build-debug
194195
- name: Build native (with Python)
195196
if: (matrix.python-version != 'fallback')
196197
env:
197198
PYO3_PYTHON: python${{ matrix.python-version }}
198199
CARGO_BUILD_TARGET: ${{ matrix.target }}
199-
run: cd packages/cubejs-backend-native && yarn run native:build-debug-python
200+
working-directory: ./packages/cubejs-backend-native
201+
run: yarn run native:build-debug-python
200202
- name: Test native (GNU only)
201203
if: (matrix.target == 'x86_64-unknown-linux-gnu')
202204
env:
203205
CUBESQL_STREAM_MODE: true
204206
CUBEJS_NATIVE_INTERNAL_DEBUG: true
205-
run: cd packages/cubejs-backend-native && yarn run test:unit
207+
working-directory: ./packages/cubejs-backend-native
208+
run: yarn run test:unit
206209
- name: Run E2E Smoke testing over whole Cube (GNU only)
207210
if: (matrix.target == 'x86_64-unknown-linux-gnu')
208211
env:
209212
CUBEJS_NATIVE_INTERNAL_DEBUG: true
210-
run: cd packages/cubejs-testing && yarn smoke:cubesql
213+
working-directory: ./packages/cubejs-testing
214+
run: yarn smoke:cubesql
211215

212216
native_macos:
213217
needs: [lint]

packages/cubejs-backend-native/js/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,13 +248,13 @@ export interface PyConfiguration {
248248
contextToApiScopes?: () => Promise<string[]>
249249
}
250250

251-
export const pythonLoadConfig = async (context: string, options: { file: string }): Promise<PyConfiguration> => {
251+
export const pythonLoadConfig = async (content: string, options: { file: string }): Promise<PyConfiguration> => {
252252
if (isFallbackBuild()) {
253253
throw new Error('Python is not supported in fallback build');
254254
}
255255

256256
const native = loadNative();
257-
const config = await native.pythonLoadConfig(context, options);
257+
const config = await native.pythonLoadConfig(content, options);
258258

259259
if (config.checkAuth) {
260260
const nativeCheckAuth = config.checkAuth;

packages/cubejs-backend-native/src/python/entry.rs

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::python::runtime::py_runtime_init;
55
use crate::python::template;
66
use neon::prelude::*;
77
use pyo3::prelude::*;
8-
use pyo3::types::{PyFunction, PyList, PyString, PyTuple};
8+
use pyo3::types::{PyDict, PyFunction, PyList, PyString, PyTuple};
99

1010
fn python_load_config(mut cx: FunctionContext) -> JsResult<JsPromise> {
1111
let config_file_content = cx.argument::<JsString>(0)?.value(&mut cx);
@@ -23,7 +23,12 @@ fn python_load_config(mut cx: FunctionContext) -> JsResult<JsPromise> {
2323
PyModule::from_code(py, cube_conf_code, "__init__.py", "cube.conf")?;
2424

2525
let config_module = PyModule::from_code(py, &config_file_content, "config.py", "")?;
26-
let settings_py = config_module.getattr("settings")?;
26+
let settings_py = if config_module.hasattr("__execution_context_locals")? {
27+
let execution_context_locals = config_module.getattr("__execution_context_locals")?;
28+
execution_context_locals.get_item("settings")?
29+
} else {
30+
config_module.getattr("settings")?
31+
};
2732

2833
let mut cube_conf = CubeConfigPy::new();
2934

@@ -61,25 +66,43 @@ fn python_load_model(mut cx: FunctionContext) -> JsResult<JsPromise> {
6166
PyModule::from_code(py, &cube_code, "__init__.py", "cube")?;
6267

6368
let model_module = PyModule::from_code(py, &model_content, &model_file_name, "")?;
64-
65-
let inspect_module = py.import("inspect")?;
66-
let args = (model_module, inspect_module.getattr("isfunction")?);
67-
let functions_with_names = inspect_module
68-
.call_method1("getmembers", args)?
69-
.downcast::<PyList>()?;
7069
let mut collected_functions = CLReprObject::new();
7170

72-
for function_details in functions_with_names.iter() {
73-
let function_details = function_details.downcast::<PyTuple>()?;
74-
let fun_name = function_details.get_item(0)?.downcast::<PyString>()?;
75-
let fun = function_details.get_item(1)?.downcast::<PyFunction>()?;
76-
77-
let has_attr = fun.hasattr("cube_context_func")?;
78-
if has_attr {
79-
let fun: Py<PyFunction> = fun.into();
80-
collected_functions.insert(fun_name.to_string(), CLRepr::PyExternalFunction(fun));
71+
if model_module.hasattr("__execution_context_locals")? {
72+
let execution_context_locals = model_module
73+
.getattr("__execution_context_locals")?
74+
.downcast::<PyDict>()?;
75+
76+
for (local_key, local_value) in execution_context_locals.iter() {
77+
if local_value.is_instance_of::<PyFunction>()? {
78+
let has_attr = local_value.hasattr("cube_context_func")?;
79+
if has_attr {
80+
let fun: Py<PyFunction> = local_value.downcast::<PyFunction>()?.into();
81+
collected_functions
82+
.insert(local_key.to_string(), CLRepr::PyExternalFunction(fun));
83+
}
84+
}
8185
}
82-
}
86+
} else {
87+
let inspect_module = py.import("inspect")?;
88+
let args = (model_module, inspect_module.getattr("isfunction")?);
89+
let functions_with_names = inspect_module
90+
.call_method1("getmembers", args)?
91+
.downcast::<PyList>()?;
92+
93+
for function_details in functions_with_names.iter() {
94+
let function_details = function_details.downcast::<PyTuple>()?;
95+
let fun_name = function_details.get_item(0)?.downcast::<PyString>()?;
96+
let fun = function_details.get_item(1)?.downcast::<PyFunction>()?;
97+
98+
let has_attr = fun.hasattr("cube_context_func")?;
99+
if has_attr {
100+
let fun: Py<PyFunction> = fun.into();
101+
collected_functions
102+
.insert(fun_name.to_string(), CLRepr::PyExternalFunction(fun));
103+
}
104+
}
105+
};
83106

84107
Ok(CubePythonModel::new(collected_functions))
85108
});

packages/cubejs-backend-native/test/jinja.test.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import * as native from '../js';
55
import type { JinjaEngine } from '../js';
66

77
const suite = native.isFallbackBuild() ? xdescribe : describe;
8+
// TODO(ovr): Find what is going wrong with parallel tests & python on Linux
9+
const darwinSuite = process.platform === 'darwin' && !native.isFallbackBuild() ? describe : xdescribe;
10+
811
const nativeInstance = new native.NativeInstance();
912

1013
function loadTemplateFile(engine: native.JinjaEngine, fileName: string): void {
@@ -13,10 +16,10 @@ function loadTemplateFile(engine: native.JinjaEngine, fileName: string): void {
1316
engine.loadTemplate(fileName, content);
1417
}
1518

16-
async function loadPythonCtxFromUtils() {
17-
const content = fs.readFileSync(path.join(process.cwd(), 'test', 'templates', 'utils.py'), 'utf8');
19+
async function loadPythonCtxFromUtils(fileName: string) {
20+
const content = fs.readFileSync(path.join(process.cwd(), 'test', 'templates', fileName), 'utf8');
1821
return nativeInstance.loadPythonContext(
19-
'utils.py',
22+
fileName,
2023
content
2124
);
2225
}
@@ -31,7 +34,7 @@ function testTemplateBySnapshot(engine: JinjaEngine, templateName: string, ctx:
3134

3235
function testTemplateWithPythonCtxBySnapshot(engine: JinjaEngine, templateName: string, ctx: unknown) {
3336
test(`render ${templateName}`, async () => {
34-
const actual = engine.renderTemplate(templateName, ctx, await loadPythonCtxFromUtils());
37+
const actual = engine.renderTemplate(templateName, ctx, await loadPythonCtxFromUtils('utils.py'));
3538

3639
expect(actual).toMatchSnapshot(templateName);
3740
});
@@ -49,9 +52,24 @@ function testLoadBrokenTemplateBySnapshot(engine: JinjaEngine, templateName: str
4952
});
5053
}
5154

52-
suite('Python models', () => {
55+
suite('Python model', () => {
5356
it('load utils.py', async () => {
54-
const pythonModule = await loadPythonCtxFromUtils();
57+
const pythonModule = await loadPythonCtxFromUtils('utils.py');
58+
59+
expect(pythonModule).toEqual({
60+
load_data: expect.any(Object),
61+
load_data_sync: expect.any(Object),
62+
arg_bool: expect.any(Object),
63+
arg_sum_integers: expect.any(Object),
64+
arg_str: expect.any(Object),
65+
arg_null: expect.any(Object),
66+
});
67+
});
68+
});
69+
70+
darwinSuite('Scope Python model', () => {
71+
it('load scoped-utils.py', async () => {
72+
const pythonModule = await loadPythonCtxFromUtils('scoped-utils.py');
5573

5674
expect(pythonModule).toEqual({
5775
load_data: expect.any(Object),

packages/cubejs-backend-native/test/python.test.ts

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,33 @@ import * as native from '../js';
55
import { PyConfiguration } from '../js';
66

77
const suite = native.isFallbackBuild() ? xdescribe : describe;
8+
// TODO(ovr): Find what is going wrong with parallel tests & python on Linux
9+
const darwinSuite = process.platform === 'darwin' && !native.isFallbackBuild() ? describe : xdescribe;
10+
11+
async function loadConfigurationFile(file: string) {
12+
const content = await fs.readFile(path.join(process.cwd(), 'test', file), 'utf8');
13+
console.log('content', {
14+
content,
15+
file
16+
});
817

9-
suite('Python', () => {
10-
async function loadConfigurationFile() {
11-
const content = await fs.readFile(path.join(process.cwd(), 'test', 'config.py'), 'utf8');
12-
console.log('content', {
13-
content
14-
});
15-
16-
const config = await native.pythonLoadConfig(
17-
content,
18-
{
19-
file: 'config.py'
20-
}
21-
);
18+
const config = await native.pythonLoadConfig(
19+
content,
20+
{
21+
file
22+
}
23+
);
2224

23-
console.log('loaded config', config);
25+
console.log(`loaded config ${file}`, config);
2426

25-
return config;
26-
}
27+
return config;
28+
}
2729

30+
suite('Python Config', () => {
2831
let config: PyConfiguration;
2932

3033
beforeAll(async () => {
31-
config = await loadConfigurationFile();
34+
config = await loadConfigurationFile('config.py');
3235
});
3336

3437
test('async checkAuth', async () => {
@@ -91,3 +94,26 @@ suite('Python', () => {
9194
);
9295
});
9396
});
97+
98+
darwinSuite('Scoped Python Config', () => {
99+
test('test', async () => {
100+
const config = await loadConfigurationFile('scoped-config.py');
101+
expect(config).toEqual({
102+
schemaPath: 'models',
103+
pgSqlPort: 5555,
104+
telemetry: false,
105+
contextToApiScopes: expect.any(Function),
106+
checkAuth: expect.any(Function),
107+
queryRewrite: expect.any(Function),
108+
});
109+
110+
if (!config.checkAuth) {
111+
throw new Error('checkAuth was not defined in config.py');
112+
}
113+
114+
await config.checkAuth(
115+
{ requestId: 'test' },
116+
'MY_SECRET_TOKEN'
117+
);
118+
});
119+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
source_code = """
2+
from cube.conf import settings
3+
4+
settings.schema_path = "models"
5+
settings.pg_sql_port = 5555
6+
settings.telemetry = False
7+
8+
def query_rewrite(query, ctx):
9+
print('[python] query_rewrite query=', query, ' ctx=', ctx)
10+
return query
11+
12+
settings.query_rewrite = query_rewrite
13+
14+
async def check_auth(req, authorization):
15+
print('[python] check_auth req=', req, ' authorization=', authorization)
16+
17+
18+
settings.check_auth = check_auth
19+
20+
async def context_to_api_scopes():
21+
print('[python] context_to_api_scopes')
22+
return ['meta', 'data', 'jobs']
23+
24+
settings.context_to_api_scopes = context_to_api_scopes
25+
"""
26+
27+
__execution_context_globals = {}
28+
__execution_context_locals = {}
29+
30+
exec(source_code, __execution_context_globals, __execution_context_locals)

0 commit comments

Comments
 (0)