diff --git a/README.md b/README.md
index 652861e552..1fa1d9b655 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,12 @@
+
+![Hactoberfestnew](https://github.com/user-attachments/assets/149a5cee-6af1-4d4e-9c43-6fcf82a9b07e)
+
+
diff --git a/frontend/taipy-gui/base/src/exports.ts b/frontend/taipy-gui/base/src/exports.ts
index 9f702a4390..ff1f70f524 100644
--- a/frontend/taipy-gui/base/src/exports.ts
+++ b/frontend/taipy-gui/base/src/exports.ts
@@ -1,7 +1,7 @@
+import { TaipyApp, createApp, OnChangeHandler, OnInitHandler } from "./app";
import { WsAdapter } from "./wsAdapter";
-// import { TaipyApp } from "./app";
+import { ModuleData } from "./dataManager";
-export {
- WsAdapter,
- // TaipyApp,
-};
+export default TaipyApp;
+export { TaipyApp, createApp, WsAdapter };
+export type { OnChangeHandler, OnInitHandler, ModuleData };
diff --git a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts
index 2df8b76559..3966d59eff 100644
--- a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts
+++ b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts
@@ -1,4 +1,4 @@
-import { Socket } from 'socket.io-client';
+import { Socket } from "socket.io-client";
export type ModuleData = Record;
export type VarName = Record;
@@ -48,10 +48,7 @@ declare class DataManager {
constructor(variableModuleData: ModuleData);
init(variableModuleData: ModuleData): ModuleData;
getEncodedName(varName: string, module: string): string | undefined;
- getName(encodedName: string): [
- string,
- string
- ] | undefined;
+ getName(encodedName: string): [string, string] | undefined;
get(encodedName: string, dataEventKey?: string): unknown;
addRequestDataOptions(encodedName: string, dataEventKey: string, options: RequestDataOptions): void;
getInfo(encodedName: string): VarData | undefined;
@@ -60,7 +57,25 @@ declare class DataManager {
update(encodedName: string, value: unknown, dataEventKey?: string): void;
deleteRequestedData(encodedName: string, dataEventKey: string): void;
}
-export type WsMessageType = "A" | "U" | "DU" | "MU" | "RU" | "AL" | "BL" | "NA" | "ID" | "MS" | "DF" | "PR" | "ACK" | "GMC" | "GDT" | "AID" | "GR" | "FV";
+export type WsMessageType =
+ | "A"
+ | "U"
+ | "DU"
+ | "MU"
+ | "RU"
+ | "AL"
+ | "BL"
+ | "NA"
+ | "ID"
+ | "MS"
+ | "DF"
+ | "PR"
+ | "ACK"
+ | "GMC"
+ | "GDT"
+ | "AID"
+ | "GR"
+ | "FV";
export interface WsMessage {
type: WsMessageType | string;
name: string;
@@ -70,17 +85,23 @@ export interface WsMessage {
module_context: string;
ack_id?: string;
}
+export declare abstract class WsAdapter {
+ abstract supportedMessageTypes: string[];
+ abstract handleWsMessage(message: WsMessage, app: TaipyApp): boolean;
+}
export type OnInitHandler = (taipyApp: TaipyApp) => void;
export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown, dataEventKey?: string) => void;
export type OnNotifyHandler = (taipyApp: TaipyApp, type: string, message: string) => void;
export type OnReloadHandler = (taipyApp: TaipyApp, removedChanges: ModuleData) => void;
export type OnWsMessage = (taipyApp: TaipyApp, event: string, payload: unknown) => void;
export type OnWsStatusUpdate = (taipyApp: TaipyApp, messageQueue: string[]) => void;
-export type Route = [
- string,
- string
-];
-export type RequestDataCallback = (taipyApp: TaipyApp, encodedName: string, dataEventKey: string, value: unknown) => void;
+export type Route = [string, string];
+export type RequestDataCallback = (
+ taipyApp: TaipyApp,
+ encodedName: string,
+ dataEventKey: string,
+ value: unknown,
+) => void;
export declare class TaipyApp {
socket: Socket;
_onInit: OnInitHandler | undefined;
@@ -100,7 +121,12 @@ export declare class TaipyApp {
path: string | undefined;
routes: Route[] | undefined;
wsAdapters: WsAdapter[];
- constructor(onInit?: OnInitHandler | undefined, onChange?: OnChangeHandler | undefined, path?: string | undefined, socket?: Socket | undefined);
+ constructor(
+ onInit?: OnInitHandler | undefined,
+ onChange?: OnChangeHandler | undefined,
+ path?: string | undefined,
+ socket?: Socket | undefined,
+ );
get onInit(): OnInitHandler | undefined;
set onInit(handler: OnInitHandler | undefined);
onInitEvent(): void;
@@ -123,10 +149,7 @@ export declare class TaipyApp {
sendWsMessage(type: WsMessageType | string, id: string, payload: unknown, context?: string | undefined): void;
registerWsAdapter(wsAdapter: WsAdapter): void;
getEncodedName(varName: string, module: string): string | undefined;
- getName(encodedName: string): [
- string,
- string
- ] | undefined;
+ getName(encodedName: string): [string, string] | undefined;
get(encodedName: string, dataEventKey?: string): unknown;
getInfo(encodedName: string): VarData | undefined;
getDataTree(): ModuleData | undefined;
@@ -144,7 +167,11 @@ export declare class TaipyApp {
getWsStatus(): string[];
getBaseUrl(): string;
}
-export declare abstract class WsAdapter {
- abstract supportedMessageTypes: string[];
- abstract handleWsMessage(message: WsMessage, app: TaipyApp): boolean;
-}
+export declare const createApp: (
+ onInit?: OnInitHandler,
+ onChange?: OnChangeHandler,
+ path?: string,
+ socket?: Socket,
+) => TaipyApp;
+
+export { TaipyApp as default };
diff --git a/frontend/taipy-gui/jest.config.js b/frontend/taipy-gui/jest.config.js
index 81649289f3..28381eb754 100644
--- a/frontend/taipy-gui/jest.config.js
+++ b/frontend/taipy-gui/jest.config.js
@@ -26,6 +26,7 @@ module.exports = {
],
coverageReporters: ["json", "html", "text"],
modulePathIgnorePatterns: ["/packaging/"],
- transformIgnorePatterns: ["/node_modules/(?!react-jsx-parser/)"],
+ moduleNameMapper: {"react-markdown": "/node_modules/react-markdown/react-markdown.min.js"},
+ transformIgnorePatterns: ["/node_modules/(?!react-jsx-parser|react-markdown/)"],
...createJsWithTsPreset()
};
diff --git a/frontend/taipy-gui/package-lock.json b/frontend/taipy-gui/package-lock.json
index a1a23d590f..ae9654a7ca 100644
--- a/frontend/taipy-gui/package-lock.json
+++ b/frontend/taipy-gui/package-lock.json
@@ -87,26 +87,6 @@
"webpack-cli": "^5.0.0"
}
},
- "node_modules/@75lb/deep-merge": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@75lb/deep-merge/-/deep-merge-1.1.2.tgz",
- "integrity": "sha512-08K9ou5VNbheZFxM5tDWoqjA3ImC50DiuuJ2tj1yEPRfkp8lLLg6XAaJ4On+a0yAXor/8ay5gHnAIshRM44Kpw==",
- "dependencies": {
- "lodash": "^4.17.21",
- "typical": "^7.1.1"
- },
- "engines": {
- "node": ">=12.17"
- }
- },
- "node_modules/@75lb/deep-merge/node_modules/typical": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/typical/-/typical-7.2.0.tgz",
- "integrity": "sha512-W1+HdVRUl8fS3MZ9ogD51GOb46xMmhAZzR0WPw5jcgIZQJVvkddYzAl4YTU6g5w33Y1iRQLdIi2/1jhi2RNL0g==",
- "engines": {
- "node": ">=12.17"
- }
- },
"node_modules/@adobe/css-tools": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz",
@@ -1912,18 +1892,18 @@
"dev": true
},
"node_modules/@mui/core-downloads-tracker": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.0.tgz",
- "integrity": "sha512-covEnIn/2er5YdtuukDRA52kmARhKrHjOvPsyTFMQApZdrTBI4h8jbEy2mxZqwMwcAFS9coonQXnEZKL1rUNdQ==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.1.tgz",
+ "integrity": "sha512-VdQC1tPIIcZAnf62L2M1eQif0x2vlKg3YK4kGYbtijSH4niEgI21GnstykW1vQIs+Bc6L+Hua2GATYVjilJ22A==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.0.tgz",
- "integrity": "sha512-HxfB0jxwiMTYMN8gAnYn3avbF1aDrqBEuGIj6JDQ3YkLl650E1Wy8AIhwwyP47wdrv0at9aAR0iOO6VLb74A9w==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.1.tgz",
+ "integrity": "sha512-sy/YKwcLPW8VcacNP2uWMYR9xyWuwO9NN9FXuGEU90bRshBXj8pdKk+joe3TCW7oviVS3zXLHlc94wQ0jNsQRQ==",
"dependencies": {
"@babel/runtime": "^7.25.6"
},
@@ -1935,7 +1915,7 @@
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
- "@mui/material": "^6.1.0",
+ "@mui/material": "^6.1.1",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
@@ -1946,15 +1926,15 @@
}
},
"node_modules/@mui/material": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.0.tgz",
- "integrity": "sha512-4MJ46vmy1xbm8x+ZdRcWm8jEMMowdS8pYlhKQzg/qoKhOcLhImZvf2Jn6z9Dj6gl+lY+C/0MxaHF/avAAGys3Q==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.1.tgz",
+ "integrity": "sha512-b+eULldTqtqTCbN++2BtBWCir/1LwEYw+2mIlOt2GiEUh1EBBw4/wIukGKKNt3xrCZqRA80yLLkV6tF61Lq3cA==",
"dependencies": {
"@babel/runtime": "^7.25.6",
- "@mui/core-downloads-tracker": "^6.1.0",
- "@mui/system": "^6.1.0",
- "@mui/types": "^7.2.16",
- "@mui/utils": "^6.1.0",
+ "@mui/core-downloads-tracker": "^6.1.1",
+ "@mui/system": "^6.1.1",
+ "@mui/types": "^7.2.17",
+ "@mui/utils": "^6.1.1",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.11",
"clsx": "^2.1.1",
@@ -1973,7 +1953,7 @@
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
- "@mui/material-pigment-css": "^6.1.0",
+ "@mui/material-pigment-css": "^6.1.1",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -1994,12 +1974,12 @@
}
},
"node_modules/@mui/private-theming": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.0.tgz",
- "integrity": "sha512-+L5qccs4gwsR0r1dgjqhN24QEQRkqIbfOdxILyMbMkuI50x6wNyt9XrV+J3WtjtZTMGJCrUa5VmZBE6OEPGPWA==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.1.tgz",
+ "integrity": "sha512-JlrjIdhyZUtewtdAuUsvi3ZnO0YS49IW4Mfz19ZWTlQ0sDGga6LNPVwHClWr2/zJK2we2BQx9/i8M32rgKuzrg==",
"dependencies": {
"@babel/runtime": "^7.25.6",
- "@mui/utils": "^6.1.0",
+ "@mui/utils": "^6.1.1",
"prop-types": "^15.8.1"
},
"engines": {
@@ -2020,9 +2000,9 @@
}
},
"node_modules/@mui/styled-engine": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.0.tgz",
- "integrity": "sha512-MZ+vtaCkjamrT41+b0Er9OMenjAtP/32+L6fARL9/+BZKuV2QbR3q3TmavT2x0NhDu35IM03s4yKqj32Ziqnyg==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.1.tgz",
+ "integrity": "sha512-HJyIoMpFb11fnHuRtUILOXgq6vj4LhIlE8maG4SwP/W+E5sa7HFexhnB3vOMT7bKys4UKNxhobC8jwWxYilGsA==",
"dependencies": {
"@babel/runtime": "^7.25.6",
"@emotion/cache": "^11.13.1",
@@ -2052,15 +2032,15 @@
}
},
"node_modules/@mui/system": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.0.tgz",
- "integrity": "sha512-NumkGDqT6EdXfcoFLYQ+M4XlTW5hH3+aK48xAbRqKPXJfxl36CBt4DLduw/Voa5dcayGus9T6jm1AwU2hoJ5hQ==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.1.tgz",
+ "integrity": "sha512-PaYsCz2tUOcpu3T0okDEsSuP/yCDIj9JZ4Tox1JovRSKIjltHpXPsXZSGr3RiWdtM1MTQMFMCZzu0+CKbyy+Kw==",
"dependencies": {
"@babel/runtime": "^7.25.6",
- "@mui/private-theming": "^6.1.0",
- "@mui/styled-engine": "^6.1.0",
- "@mui/types": "^7.2.16",
- "@mui/utils": "^6.1.0",
+ "@mui/private-theming": "^6.1.1",
+ "@mui/styled-engine": "^6.1.1",
+ "@mui/types": "^7.2.17",
+ "@mui/utils": "^6.1.1",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
@@ -2091,9 +2071,9 @@
}
},
"node_modules/@mui/types": {
- "version": "7.2.16",
- "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.16.tgz",
- "integrity": "sha512-qI8TV3M7ShITEEc8Ih15A2vLzZGLhD+/UPNwck/hcls2gwg7dyRjNGXcQYHKLB5Q7PuTRfrTkAoPa2VV1s67Ag==",
+ "version": "7.2.17",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.17.tgz",
+ "integrity": "sha512-oyumoJgB6jDV8JFzRqjBo2daUuHpzDjoO/e3IrRhhHo/FxJlaVhET6mcNrKHUq2E+R+q3ql0qAtvQ4rfWHhAeQ==",
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
@@ -2104,12 +2084,12 @@
}
},
"node_modules/@mui/utils": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.0.tgz",
- "integrity": "sha512-oT8ZzMISRUhTVpdbYzY0CgrCBb3t/YEdcaM13tUnuTjZ15pdA6g5lx15ZJUdgYXV6PbJdw7tDQgMEr4uXK5TXQ==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.1.tgz",
+ "integrity": "sha512-HlRrgdJSPbYDXPpoVMWZV8AE7WcFtAk13rWNWAEVWKSanzBBkymjz3km+Th/Srowsh4pf1fTSP1B0L116wQBYw==",
"dependencies": {
"@babel/runtime": "^7.25.6",
- "@mui/types": "^7.2.16",
+ "@mui/types": "^7.2.17",
"@types/prop-types": "^15.7.12",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
@@ -2954,9 +2934,9 @@
}
},
"node_modules/@types/estree": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
- "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
},
"node_modules/@types/estree-jsx": {
"version": "1.0.5",
@@ -4561,9 +4541,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001660",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz",
- "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==",
+ "version": "1.0.30001662",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001662.tgz",
+ "integrity": "sha512-sgMUVwLmGseH8ZIrm1d51UbrhqMCH3jvS7gF/M6byuHOnKyLOBL7W8yz5V02OHwgLGA36o/AFhWzzh4uc5aqTA==",
"funding": [
{
"type": "opencollective",
@@ -5080,13 +5060,13 @@
}
},
"node_modules/command-line-usage": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.1.tgz",
- "integrity": "sha512-NCyznE//MuTjwi3y84QVUGEOT+P5oto1e1Pk/jFPVdPPfsG03qpTIl3yw6etR+v73d0lXsoojRpvbru2sqePxQ==",
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.3.tgz",
+ "integrity": "sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==",
"dependencies": {
"array-back": "^6.2.2",
"chalk-template": "^0.4.0",
- "table-layout": "^3.0.0",
+ "table-layout": "^4.1.0",
"typical": "^7.1.1"
},
"engines": {
@@ -6135,9 +6115,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.24",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.24.tgz",
- "integrity": "sha512-0x0wLCmpdKFCi9ulhvYZebgcPmHTkFVUfU2wzDykadkslKwT4oAmDTHEKLnlrDsMGZe4B+ksn8quZfZjYsBetA=="
+ "version": "1.5.25",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.25.tgz",
+ "integrity": "sha512-kMb204zvK3PsSlgvvwzI3wBIcAw15tRkYk+NQdsjdDtcQWTp2RABbMQ9rUBy8KNEOM+/E6ep+XC3AykiWZld4g=="
},
"node_modules/element-size": {
"version": "1.1.1",
@@ -14757,14 +14737,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
- "node_modules/stream-read-all": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/stream-read-all/-/stream-read-all-3.0.1.tgz",
- "integrity": "sha512-EWZT9XOceBPlVJRrYcykW8jyRSZYbkb/0ZK36uLEmoWVO5gxBOnntNTseNzfREsqxqdfEGQrD8SXQ3QWbBmq8A==",
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/stream-shift": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
@@ -15125,21 +15097,13 @@
"dev": true
},
"node_modules/table-layout": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-3.0.2.tgz",
- "integrity": "sha512-rpyNZYRw+/C+dYkcQ3Pr+rLxW4CfHpXjPDnG7lYhdRoUcZTUt+KEsX+94RGp/aVp/MQU35JCITv2T/beY4m+hw==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz",
+ "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==",
"dependencies": {
- "@75lb/deep-merge": "^1.1.1",
"array-back": "^6.2.2",
- "command-line-args": "^5.2.1",
- "command-line-usage": "^7.0.0",
- "stream-read-all": "^3.0.1",
- "typical": "^7.1.1",
"wordwrapjs": "^5.1.0"
},
- "bin": {
- "table-layout": "bin/cli.js"
- },
"engines": {
"node": ">=12.17"
}
@@ -15152,14 +15116,6 @@
"node": ">=12.17"
}
},
- "node_modules/table-layout/node_modules/typical": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/typical/-/typical-7.2.0.tgz",
- "integrity": "sha512-W1+HdVRUl8fS3MZ9ogD51GOb46xMmhAZzR0WPw5jcgIZQJVvkddYzAl4YTU6g5w33Y1iRQLdIi2/1jhi2RNL0g==",
- "engines": {
- "node": ">=12.17"
- }
- },
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
diff --git a/frontend/taipy-gui/src/components/Taipy/Chat.spec.tsx b/frontend/taipy-gui/src/components/Taipy/Chat.spec.tsx
index aaebc2b912..7348c411eb 100644
--- a/frontend/taipy-gui/src/components/Taipy/Chat.spec.tsx
+++ b/frontend/taipy-gui/src/components/Taipy/Chat.spec.tsx
@@ -12,7 +12,7 @@
*/
import React from "react";
-import { render } from "@testing-library/react";
+import { render, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom";
import userEvent from "@testing-library/user-event";
@@ -39,48 +39,63 @@ const searchMsg = messages[valueKey].data[0][1];
describe("Chat Component", () => {
it("renders", async () => {
- const { getByText, getByLabelText } = render();
+ const { getByText, getByLabelText } = render();
const elt = getByText(searchMsg);
expect(elt.tagName).toBe("DIV");
const input = getByLabelText("message (taipy)");
expect(input.tagName).toBe("INPUT");
});
it("uses the class", async () => {
- const { getByText } = render();
+ const { getByText } = render();
const elt = getByText(searchMsg);
expect(elt.parentElement?.parentElement?.parentElement?.parentElement?.parentElement?.parentElement).toHaveClass("taipy-chat");
});
it("can display an avatar", async () => {
- const { getByAltText } = render();
+ const { getByAltText } = render();
const elt = getByAltText("Fred.png");
expect(elt.tagName).toBe("IMG");
});
it("is disabled", async () => {
- const { getAllByRole } = render();
+ const { getAllByRole } = render();
const elts = getAllByRole("button");
elts.forEach((elt) => expect(elt).toHaveClass("Mui-disabled"));
});
it("is enabled by default", async () => {
- const { getAllByRole } = render();
+ const { getAllByRole } = render();
const elts = getAllByRole("button");
elts.forEach((elt) => expect(elt).not.toHaveClass("Mui-disabled"));
});
it("is enabled by active", async () => {
- const { getAllByRole } = render();
+ const { getAllByRole } = render();
const elts = getAllByRole("button");
elts.forEach((elt) => expect(elt).not.toHaveClass("Mui-disabled"));
});
it("can hide input", async () => {
- render();
+ render();
const elt = document.querySelector(".taipy-chat input");
expect(elt).toBeNull();
});
+ it("renders markdown by default", async () => {
+ render();
+ const elt = document.querySelector(".taipy-chat .taipy-chat-received .MuiPaper-root");
+ await waitFor(() => expect(elt?.querySelector("p")).not.toBeNull());
+ });
+ it("can render pre", async () => {
+ render();
+ const elt = document.querySelector(".taipy-chat .taipy-chat-received .MuiPaper-root pre");
+ expect(elt).toBeInTheDocument();
+ });
+ it("can render raw", async () => {
+ render();
+ const elt = document.querySelector(".taipy-chat .taipy-chat-received div.MuiPaper-root");
+ expect(elt).toBeInTheDocument();
+ });
it("dispatch a well formed message by Keyboard", async () => {
const dispatch = jest.fn();
const state: TaipyState = INITIAL_STATE;
const { getByLabelText } = render(
-
+
);
const elt = getByLabelText("message (taipy)");
@@ -92,7 +107,7 @@ describe("Chat Component", () => {
context: undefined,
payload: {
action: undefined,
- args: ["Enter", "varname", "new message", "taipy"],
+ args: ["Enter", "varName", "new message", "taipy"],
},
});
});
@@ -101,7 +116,7 @@ describe("Chat Component", () => {
const state: TaipyState = INITIAL_STATE;
const { getByLabelText, getByRole } = render(
-
+
);
const elt = getByLabelText("message (taipy)");
@@ -114,7 +129,7 @@ describe("Chat Component", () => {
context: undefined,
payload: {
action: undefined,
- args: ["click", "varname", "new message", "taipy"],
+ args: ["click", "varName", "new message", "taipy"],
},
});
});
diff --git a/frontend/taipy-gui/src/components/Taipy/Chat.tsx b/frontend/taipy-gui/src/components/Taipy/Chat.tsx
index 38f3c51056..44449b99a8 100644
--- a/frontend/taipy-gui/src/components/Taipy/Chat.tsx
+++ b/frontend/taipy-gui/src/components/Taipy/Chat.tsx
@@ -11,7 +11,7 @@
* specific language governing permissions and limitations under the License.
*/
-import React, { useMemo, useCallback, KeyboardEvent, MouseEvent, useState, useRef, useEffect, ReactNode } from "react";
+import React, { useMemo, useCallback, KeyboardEvent, MouseEvent, useState, useRef, useEffect, ReactNode, lazy } from "react";
import { SxProps, Theme, darken, lighten } from "@mui/material/styles";
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
@@ -28,8 +28,6 @@ import Send from "@mui/icons-material/Send";
import ArrowDownward from "@mui/icons-material/ArrowDownward";
import ArrowUpward from "@mui/icons-material/ArrowUpward";
-// import InfiniteLoader from "react-window-infinite-loader";
-
import { createRequestInfiniteTableUpdateAction, createSendActionNameAction } from "../../context/taipyReducers";
import { TaipyActiveProps, disableColor, getSuffixedClassNames } from "./utils";
import { useClassNames, useDispatch, useDynamicProperty, useElementVisible, useModule } from "../../utils/hooks";
@@ -39,6 +37,8 @@ import { emptyArray, getInitials } from "../../utils";
import { RowType, TableValueType } from "./tableUtils";
import { Stack } from "@mui/material";
+const Markdown = lazy(() => import("react-markdown"));
+
interface ChatProps extends TaipyActiveProps {
messages?: TableValueType;
withInput?: boolean;
@@ -50,6 +50,7 @@ interface ChatProps extends TaipyActiveProps {
defaultKey?: string; // for testing purposes only
pageSize?: number;
showSender?: boolean;
+ mode?: string;
}
const ENTER_KEY = "Enter";
@@ -66,7 +67,13 @@ const gridSx = { pb: "1em", mt: "unset", flex: 1, overflow: "auto" };
const loadMoreSx = { width: "fit-content", marginLeft: "auto", marginRight: "auto" };
const inputSx = { maxWidth: "unset" };
const leftNameSx = { fontSize: "0.6em", fontWeight: "bolder", pl: `${indicWidth}em` };
-const rightNameSx: SxProps = { ...leftNameSx, pr: `${2 * indicWidth}em`, width: "100%", display: "flex", justifyContent: "flex-end" };
+const rightNameSx: SxProps = {
+ ...leftNameSx,
+ pr: `${2 * indicWidth}em`,
+ width: "100%",
+ display: "flex",
+ justifyContent: "flex-end",
+};
const senderPaperSx = {
pr: `${indicWidth}em`,
pl: `${indicWidth}em`,
@@ -127,10 +134,11 @@ interface ChatRowProps {
getAvatar: (id: string, sender: boolean) => ReactNode;
index: number;
showSender: boolean;
+ mode?: string;
}
const ChatRow = (props: ChatRowProps) => {
- const { senderId, message, name, className, getAvatar, index, showSender } = props;
+ const { senderId, message, name, className, getAvatar, index, showSender, mode } = props;
const sender = senderId == name;
const avatar = getAvatar(name, sender);
@@ -149,14 +157,26 @@ const ChatRow = (props: ChatRowProps) => {
{name}
- {message}
+ {mode == "pre" ? (
+ {message}
+ ) : mode == "raw" ? (
+ message
+ ) : (
+ {message}
+ )}
{sender ? {avatar} : null}
) : (
- {message}
+ {mode == "pre" ? (
+ {message}
+ ) : mode == "raw" ? (
+ message
+ ) : (
+ {message}
+ )}
)}
@@ -385,6 +405,7 @@ const Chat = (props: ChatProps) => {
getAvatar={getAvatar}
index={idx}
showSender={showSender}
+ mode={props.mode}
/>
) : null
)}
@@ -406,19 +427,21 @@ const Chat = (props: ChatProps) => {
label={`message (${senderId})`}
disabled={!active}
onKeyDown={handleAction}
- InputProps={{
- endAdornment: (
-
-
-
-
-
- ),
+ slotProps={{
+ input: {
+ endAdornment: (
+
+
+
+
+
+ ),
+ },
}}
sx={inputSx}
/>
diff --git a/frontend/taipy-gui/src/components/Taipy/Field.spec.tsx b/frontend/taipy-gui/src/components/Taipy/Field.spec.tsx
index cb16a03fc3..d42de9e77f 100644
--- a/frontend/taipy-gui/src/components/Taipy/Field.spec.tsx
+++ b/frontend/taipy-gui/src/components/Taipy/Field.spec.tsx
@@ -12,7 +12,7 @@
*/
import React from "react";
-import { render } from "@testing-library/react";
+import { render, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom";
import Field from "./Field";
@@ -60,4 +60,14 @@ describe("Field Component", () => {
const elt = getByText("titi");
expect(elt).toHaveStyle("width: 500px");
});
+ it("can render markdown", async () => {
+ render();
+ const elt = document.querySelector(".taipy-text");
+ await waitFor(() => expect(elt?.querySelector("p")).not.toBeNull());
+ });
+ it("can render pre", async () => {
+ render();
+ const elt = document.querySelector("pre.taipy-text");
+ expect(elt).toBeInTheDocument();
+ });
});
diff --git a/frontend/taipy-gui/src/components/Taipy/Input.tsx b/frontend/taipy-gui/src/components/Taipy/Input.tsx
index 2dc9de6219..dba0d31d53 100644
--- a/frontend/taipy-gui/src/components/Taipy/Input.tsx
+++ b/frontend/taipy-gui/src/components/Taipy/Input.tsx
@@ -260,45 +260,58 @@ const Input = (props: TaipyInputProps) => {
(event: React.MouseEvent) => event.preventDefault(),
[]
);
- const muiInputProps = useMemo(
+ const inputProps = useMemo(
() =>
- type == "password"
+ type == "number"
? {
- endAdornment: (
-
- {showPassword ? : }
-
- ),
+ htmlInput: {
+ step: step ? step : 1,
+ min: min,
+ max: max,
+ },
+ input: {
+ endAdornment: (
+
+ ),
+ },
}
- : type == "number"
- ? {
- endAdornment: (
-
- ),
- }
- : undefined,
+ : type == "password"
+ ? {
+ htmlInput: { autoComplete: "current-password" },
+ input: {
+ endAdornment: (
+
+ {showPassword ? : }
+
+ ),
+ },
+ }
+ : undefined,
[
type,
+ step,
+ min,
+ max,
showPassword,
handleClickShowPassword,
handleMouseDownPassword,
@@ -307,20 +320,6 @@ const Input = (props: TaipyInputProps) => {
]
);
- const inputProps = useMemo(
- () =>
- type == "number"
- ? {
- step: step ? step : 1,
- min: min,
- max: max,
- }
- : type == "password"
- ? { autoComplete: "current-password" }
- : undefined,
- [type, step, min, max]
- );
-
useEffect(() => {
if (props.value !== undefined) {
setValue(props.value);
@@ -337,10 +336,7 @@ const Input = (props: TaipyInputProps) => {
className={className}
type={showPassword && type == "password" ? "text" : type}
id={id}
- slotProps={{
- htmlInput: inputProps,
- input: muiInputProps,
- }}
+ slotProps={inputProps}
label={props.label}
onChange={handleInput}
disabled={!active}
diff --git a/frontend/taipy-gui/src/components/Taipy/Login.tsx b/frontend/taipy-gui/src/components/Taipy/Login.tsx
index aa37371f2d..a2058732c6 100644
--- a/frontend/taipy-gui/src/components/Taipy/Login.tsx
+++ b/frontend/taipy-gui/src/components/Taipy/Login.tsx
@@ -48,7 +48,7 @@ const closeSx: SxProps = {
alignSelf: "start",
};
const titleSx = { m: 0, p: 2, display: "flex", paddingRight: "0.1em" };
-const userProps = { autoComplete: "username" };
+const userProps = { htmlInput: { autoComplete: "username" }};
const pwdProps = { autoComplete: "current-password" };
const Login = (props: LoginProps) => {
@@ -97,7 +97,7 @@ const Login = (props: LoginProps) => {
[]
);
const passwordProps = useMemo(
- () => ({
+ () => ({input: {
endAdornment: (
{
),
- }),
+ }, htmlInput: pwdProps}),
[showPassword, handleClickShowPassword, handleMouseDownPassword]
);
@@ -145,7 +145,7 @@ const Login = (props: LoginProps) => {
onChange={changeInput}
data-input="user"
onKeyDown={handleEnter}
- inputProps={userProps}
+ slotProps={userProps}
>
{
onChange={changeInput}
data-input="password"
onKeyDown={handleEnter}
- inputProps={pwdProps}
- InputProps={passwordProps}
+ slotProps={passwordProps}
/>
{message || defaultMessage}
diff --git a/frontend/taipy/package-lock.json b/frontend/taipy/package-lock.json
index c5466b3b46..a38a3cad5c 100644
--- a/frontend/taipy/package-lock.json
+++ b/frontend/taipy/package-lock.json
@@ -660,18 +660,18 @@
"dev": true
},
"node_modules/@mui/core-downloads-tracker": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.0.tgz",
- "integrity": "sha512-covEnIn/2er5YdtuukDRA52kmARhKrHjOvPsyTFMQApZdrTBI4h8jbEy2mxZqwMwcAFS9coonQXnEZKL1rUNdQ==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.1.tgz",
+ "integrity": "sha512-VdQC1tPIIcZAnf62L2M1eQif0x2vlKg3YK4kGYbtijSH4niEgI21GnstykW1vQIs+Bc6L+Hua2GATYVjilJ22A==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.0.tgz",
- "integrity": "sha512-HxfB0jxwiMTYMN8gAnYn3avbF1aDrqBEuGIj6JDQ3YkLl650E1Wy8AIhwwyP47wdrv0at9aAR0iOO6VLb74A9w==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.1.tgz",
+ "integrity": "sha512-sy/YKwcLPW8VcacNP2uWMYR9xyWuwO9NN9FXuGEU90bRshBXj8pdKk+joe3TCW7oviVS3zXLHlc94wQ0jNsQRQ==",
"dependencies": {
"@babel/runtime": "^7.25.6"
},
@@ -683,7 +683,7 @@
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
- "@mui/material": "^6.1.0",
+ "@mui/material": "^6.1.1",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
@@ -694,15 +694,15 @@
}
},
"node_modules/@mui/material": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.0.tgz",
- "integrity": "sha512-4MJ46vmy1xbm8x+ZdRcWm8jEMMowdS8pYlhKQzg/qoKhOcLhImZvf2Jn6z9Dj6gl+lY+C/0MxaHF/avAAGys3Q==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.1.tgz",
+ "integrity": "sha512-b+eULldTqtqTCbN++2BtBWCir/1LwEYw+2mIlOt2GiEUh1EBBw4/wIukGKKNt3xrCZqRA80yLLkV6tF61Lq3cA==",
"dependencies": {
"@babel/runtime": "^7.25.6",
- "@mui/core-downloads-tracker": "^6.1.0",
- "@mui/system": "^6.1.0",
- "@mui/types": "^7.2.16",
- "@mui/utils": "^6.1.0",
+ "@mui/core-downloads-tracker": "^6.1.1",
+ "@mui/system": "^6.1.1",
+ "@mui/types": "^7.2.17",
+ "@mui/utils": "^6.1.1",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.11",
"clsx": "^2.1.1",
@@ -721,7 +721,7 @@
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
- "@mui/material-pigment-css": "^6.1.0",
+ "@mui/material-pigment-css": "^6.1.1",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -742,12 +742,12 @@
}
},
"node_modules/@mui/private-theming": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.0.tgz",
- "integrity": "sha512-+L5qccs4gwsR0r1dgjqhN24QEQRkqIbfOdxILyMbMkuI50x6wNyt9XrV+J3WtjtZTMGJCrUa5VmZBE6OEPGPWA==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.1.tgz",
+ "integrity": "sha512-JlrjIdhyZUtewtdAuUsvi3ZnO0YS49IW4Mfz19ZWTlQ0sDGga6LNPVwHClWr2/zJK2we2BQx9/i8M32rgKuzrg==",
"dependencies": {
"@babel/runtime": "^7.25.6",
- "@mui/utils": "^6.1.0",
+ "@mui/utils": "^6.1.1",
"prop-types": "^15.8.1"
},
"engines": {
@@ -768,9 +768,9 @@
}
},
"node_modules/@mui/styled-engine": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.0.tgz",
- "integrity": "sha512-MZ+vtaCkjamrT41+b0Er9OMenjAtP/32+L6fARL9/+BZKuV2QbR3q3TmavT2x0NhDu35IM03s4yKqj32Ziqnyg==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.1.tgz",
+ "integrity": "sha512-HJyIoMpFb11fnHuRtUILOXgq6vj4LhIlE8maG4SwP/W+E5sa7HFexhnB3vOMT7bKys4UKNxhobC8jwWxYilGsA==",
"dependencies": {
"@babel/runtime": "^7.25.6",
"@emotion/cache": "^11.13.1",
@@ -800,15 +800,15 @@
}
},
"node_modules/@mui/system": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.0.tgz",
- "integrity": "sha512-NumkGDqT6EdXfcoFLYQ+M4XlTW5hH3+aK48xAbRqKPXJfxl36CBt4DLduw/Voa5dcayGus9T6jm1AwU2hoJ5hQ==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.1.tgz",
+ "integrity": "sha512-PaYsCz2tUOcpu3T0okDEsSuP/yCDIj9JZ4Tox1JovRSKIjltHpXPsXZSGr3RiWdtM1MTQMFMCZzu0+CKbyy+Kw==",
"dependencies": {
"@babel/runtime": "^7.25.6",
- "@mui/private-theming": "^6.1.0",
- "@mui/styled-engine": "^6.1.0",
- "@mui/types": "^7.2.16",
- "@mui/utils": "^6.1.0",
+ "@mui/private-theming": "^6.1.1",
+ "@mui/styled-engine": "^6.1.1",
+ "@mui/types": "^7.2.17",
+ "@mui/utils": "^6.1.1",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
@@ -839,9 +839,9 @@
}
},
"node_modules/@mui/types": {
- "version": "7.2.16",
- "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.16.tgz",
- "integrity": "sha512-qI8TV3M7ShITEEc8Ih15A2vLzZGLhD+/UPNwck/hcls2gwg7dyRjNGXcQYHKLB5Q7PuTRfrTkAoPa2VV1s67Ag==",
+ "version": "7.2.17",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.17.tgz",
+ "integrity": "sha512-oyumoJgB6jDV8JFzRqjBo2daUuHpzDjoO/e3IrRhhHo/FxJlaVhET6mcNrKHUq2E+R+q3ql0qAtvQ4rfWHhAeQ==",
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
@@ -852,12 +852,12 @@
}
},
"node_modules/@mui/utils": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.0.tgz",
- "integrity": "sha512-oT8ZzMISRUhTVpdbYzY0CgrCBb3t/YEdcaM13tUnuTjZ15pdA6g5lx15ZJUdgYXV6PbJdw7tDQgMEr4uXK5TXQ==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.1.tgz",
+ "integrity": "sha512-HlRrgdJSPbYDXPpoVMWZV8AE7WcFtAk13rWNWAEVWKSanzBBkymjz3km+Th/Srowsh4pf1fTSP1B0L116wQBYw==",
"dependencies": {
"@babel/runtime": "^7.25.6",
- "@mui/types": "^7.2.16",
+ "@mui/types": "^7.2.17",
"@types/prop-types": "^15.7.12",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
@@ -1223,9 +1223,9 @@
}
},
"node_modules/@types/estree": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
- "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true
},
"node_modules/@types/hoist-non-react-statics": {
@@ -2080,9 +2080,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001660",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz",
- "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==",
+ "version": "1.0.30001662",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001662.tgz",
+ "integrity": "sha512-sgMUVwLmGseH8ZIrm1d51UbrhqMCH3jvS7gF/M6byuHOnKyLOBL7W8yz5V02OHwgLGA36o/AFhWzzh4uc5aqTA==",
"dev": true,
"funding": [
{
@@ -2409,9 +2409,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.24",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.24.tgz",
- "integrity": "sha512-0x0wLCmpdKFCi9ulhvYZebgcPmHTkFVUfU2wzDykadkslKwT4oAmDTHEKLnlrDsMGZe4B+ksn8quZfZjYsBetA==",
+ "version": "1.5.25",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.25.tgz",
+ "integrity": "sha512-kMb204zvK3PsSlgvvwzI3wBIcAw15tRkYk+NQdsjdDtcQWTp2RABbMQ9rUBy8KNEOM+/E6ep+XC3AykiWZld4g==",
"dev": true
},
"node_modules/enhanced-resolve": {
diff --git a/frontend/taipy/src/DataNodeViewer.tsx b/frontend/taipy/src/DataNodeViewer.tsx
index 001df68c7e..fe523ae609 100644
--- a/frontend/taipy/src/DataNodeViewer.tsx
+++ b/frontend/taipy/src/DataNodeViewer.tsx
@@ -786,7 +786,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
sx={FieldNoMaxWidth}
value={label || ""}
onChange={onLabelChange}
- InputProps={{
+ slotProps={{input:{
endAdornment: (
@@ -809,7 +809,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
),
- }}
+ }}}
disabled={!valid}
/>
) : (
@@ -1067,7 +1067,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
? "number"
: undefined
}
- InputProps={{
+ slotProps={{input: {
endAdornment: (
@@ -1090,7 +1090,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
),
- }}
+ }}}
disabled={!valid}
/>
)}
diff --git a/frontend/taipy/src/PropertiesEditor.tsx b/frontend/taipy/src/PropertiesEditor.tsx
index 7e9b2f1c1e..92655ee174 100644
--- a/frontend/taipy/src/PropertiesEditor.tsx
+++ b/frontend/taipy/src/PropertiesEditor.tsx
@@ -206,7 +206,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
data-name="key"
data-id={property.id}
onChange={updatePropertyField}
- inputProps={{ onKeyDown }}
+ slotProps={{ input: { onKeyDown } }}
/>
@@ -219,7 +219,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
data-name="value"
data-id={property.id}
onChange={updatePropertyField}
- inputProps={{ onKeyDown, "data-enter": true }}
+ slotProps={{ htmlInput: { onKeyDown, "data-enter": true } }}
/>
{
variant="outlined"
sx={FieldNoMaxWidth}
disabled={!isDefined}
- inputProps={{ onKeyDown }}
+ slotProps={{ htmlInput: { onKeyDown } }}
/>
@@ -321,7 +321,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
variant="outlined"
sx={FieldNoMaxWidth}
disabled={!isDefined}
- inputProps={{ onKeyDown, "data-enter": true }}
+ slotProps={{ htmlInput: { onKeyDown, "data-enter": true }}}
/>
diff --git a/frontend/taipy/src/ScenarioViewer.tsx b/frontend/taipy/src/ScenarioViewer.tsx
index 06f0c925a7..c5a26d3a7d 100644
--- a/frontend/taipy/src/ScenarioViewer.tsx
+++ b/frontend/taipy/src/ScenarioViewer.tsx
@@ -629,7 +629,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
expandIcon={expandable ? : null}
sx={AccordionSummarySx}
>
-
+
{scLabel}
{scPrimary ? (
@@ -712,7 +712,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
sx={FieldNoMaxWidth}
value={label || ""}
onChange={onLabelChange}
- InputProps={{
+ slotProps={{input: {
onKeyDown: onLabelKeyDown,
endAdornment: (
@@ -736,7 +736,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
),
- }}
+ }}}
disabled={!valid}
/>
) : (
@@ -752,7 +752,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
{showTags ? (
{
label="Tags"
sx={tagsAutocompleteSx}
fullWidth
- InputProps={{
+ slotProps={{input: {
...params.InputProps,
onKeyDown: onTagsKeyDown,
endAdornment: (
@@ -812,7 +812,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
>
),
- }}
+ }}}
/>
)}
disabled={!valid}
diff --git a/taipy/core/_core.py b/taipy/core/_core.py
new file mode 100644
index 0000000000..e8883f408e
--- /dev/null
+++ b/taipy/core/_core.py
@@ -0,0 +1,29 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+from taipy.logger._taipy_logger import _TaipyLogger
+
+from .common._warnings import _warn_deprecated
+from .orchestrator import Orchestrator
+
+
+class Core:
+ """Deprecated. Use the `Orchestrator^` service class with `taipy.Orchestrator()` instead."""
+
+ __logger = _TaipyLogger._get_logger()
+
+ def __new__(cls) -> Orchestrator: # type: ignore
+ _warn_deprecated("'Core'", suggest="the 'Orchestrator' class")
+ cls.__logger.warning(
+ "The `Core` service is deprecated and replaced by the `Orchestrator` service. "
+ "An `Orchestrator` instance has been instantiated instead."
+ )
+ return Orchestrator()
diff --git a/taipy/core/_init.py b/taipy/core/_init.py
index bf1cc077f4..48f0a65929 100644
--- a/taipy/core/_init.py
+++ b/taipy/core/_init.py
@@ -9,6 +9,7 @@
# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.
+from ._core import Core
from ._entity.submittable import Submittable
from .cycle.cycle import Cycle
from .cycle.cycle_id import CycleId
diff --git a/taipy/core/job/_job_manager.py b/taipy/core/job/_job_manager.py
index 0f64d4d618..10deec2cf1 100644
--- a/taipy/core/job/_job_manager.py
+++ b/taipy/core/job/_job_manager.py
@@ -94,7 +94,7 @@ def _is_deletable(cls, job: Union[Job, JobId]) -> ReasonCollection:
if isinstance(job, str):
job = cls._get(job)
- if not job.is_finished():
+ if job and not job.is_finished():
reason_collector._add_reason(job.id, JobIsNotFinished(job.id))
return reason_collector
diff --git a/taipy/gui/_gui_section.py b/taipy/gui/_gui_section.py
index d062801259..7ce5e517fa 100644
--- a/taipy/gui/_gui_section.py
+++ b/taipy/gui/_gui_section.py
@@ -19,7 +19,7 @@
class _GuiSection(UniqueSection):
- name = "gui"
+ name = "gui" # type: ignore[reportAssignmentType]
def __init__(self, property_list: t.Optional[t.List] = None, **properties):
self._property_list = property_list
@@ -37,13 +37,14 @@ def _to_dict(self):
return as_dict
@classmethod
- def _from_dict(cls, as_dict: t.Dict[str, t.Any], *_):
- return _GuiSection(property_list=list(default_config), **as_dict)
+ def _from_dict(cls, config_as_dict: t.Dict[str, t.Any], id, config):
+ return _GuiSection(property_list=list(default_config), **config_as_dict)
def _update(self, config_as_dict: t.Dict[str, t.Any], default_section=None):
+ as_dict = None
if self._property_list:
as_dict = {k: v for k, v in config_as_dict.items() if k in self._property_list}
- self._properties.update(as_dict)
+ self._properties.update(as_dict or config_as_dict)
@staticmethod
def _configure(**properties) -> "_GuiSection":
diff --git a/taipy/gui/_page.py b/taipy/gui/_page.py
index 8decae1933..26b98dcd79 100644
--- a/taipy/gui/_page.py
+++ b/taipy/gui/_page.py
@@ -25,7 +25,7 @@ class _Page(object):
def __init__(self) -> None:
self._rendered_jsx: t.Optional[str] = None
self._renderer: t.Optional[Page] = None
- self._style: t.Optional[str] = None
+ self._style: t.Optional[t.Union[str, t.Dict[str, t.Any]]] = None
self._route: t.Optional[str] = None
self._head: t.Optional[list] = None
diff --git a/taipy/gui/_renderers/factory.py b/taipy/gui/_renderers/factory.py
index 5613b1b1ca..bcaaaed3cb 100644
--- a/taipy/gui/_renderers/factory.py
+++ b/taipy/gui/_renderers/factory.py
@@ -99,6 +99,7 @@ class _Factory:
("height",),
("page_size", PropertyType.number, 50),
("show_sender", PropertyType.boolean, False),
+ ("mode",),
]
),
"chart": lambda gui, control_type, attrs: _Builder(
@@ -683,7 +684,7 @@ def call_builder(
builder = _Factory.__CONTROL_BUILDERS.get(name)
built = None
_Factory.__COUNTER += 1
- with gui._get_autorization():
+ with gui._get_authorization():
if builder is None:
lib, element_name, element = _Factory.__get_library_element(name)
if lib:
diff --git a/taipy/gui/config.py b/taipy/gui/config.py
index eca991afe1..00dda222ea 100644
--- a/taipy/gui/config.py
+++ b/taipy/gui/config.py
@@ -162,14 +162,16 @@ def _load(self, config: Config) -> None:
self.get_time_zone()
def _get_config(self, name: ConfigParameter, default_value: t.Any) -> t.Any: # pragma: no cover
- if name in self.config and self.config[name] is not None:
- if default_value is not None and not isinstance(self.config[name], type(default_value)):
+ if name in self.config and self.config.get(name) is not None:
+ if default_value is not None and not isinstance(self.config.get(name), type(default_value)):
try:
- return type(default_value)(self.config[name])
+ return type(default_value)(self.config.get(name))
except Exception as e:
- _warn(f'app_config "{name}" value "{self.config[name]}" is not of type {type(default_value)}', e)
+ _warn(
+ f'app_config "{name}" value "{self.config.get(name)}" is not of type {type(default_value)}', e
+ )
return default_value
- return self.config[name]
+ return self.config.get(name)
return default_value
def get_time_zone(self) -> t.Optional[str]:
@@ -234,12 +236,12 @@ def _build_config(self, root_dir, env_filename, kwargs): # pragma: no cover
config[key] = _default_stylekit if value else {}
continue
try:
- if isinstance(value, dict) and isinstance(config[key], dict):
- config[key].update(value)
+ if isinstance(value, dict) and isinstance(config.get(key), dict):
+ t.cast(dict, config.get(key)).update(value)
elif key == "port" and str(value).strip() == "auto":
config["port"] = "auto"
else:
- config[key] = value if config[key] is None else type(config[key])(value)
+ config[key] = value if config.get(key) is None else type(config.get(key))(value) # type: ignore[reportCallIssue]
except Exception as e:
_warn(
f"Invalid keyword arguments value in Gui.run {key} - {value}. Unable to parse value to the correct type", # noqa: E501
@@ -282,29 +284,29 @@ def resolve(self):
app_config = self.config
logger = _TaipyLogger._get_logger()
# Special config for notebook runtime
- if _is_in_notebook() or app_config["run_in_thread"] and not app_config["single_client"]:
+ if _is_in_notebook() or app_config.get("run_in_thread") and not app_config.get("single_client"):
app_config["single_client"] = True
self.__log_outside_reloader(logger, "Running in 'single_client' mode in notebook environment")
- if app_config["run_server"] and app_config["ngrok_token"] and app_config["use_reloader"]:
+ if app_config.get("run_server") and app_config.get("ngrok_token") and app_config.get("use_reloader"):
app_config["use_reloader"] = False
self.__log_outside_reloader(
logger, "'use_reloader' parameter will not be used when 'ngrok_token' parameter is available"
)
- if app_config["use_reloader"] and _is_in_notebook():
+ if app_config.get("use_reloader") and _is_in_notebook():
app_config["use_reloader"] = False
self.__log_outside_reloader(logger, "'use_reloader' parameter is not available in notebook environment")
- if app_config["use_reloader"] and not app_config["debug"]:
+ if app_config.get("use_reloader") and not app_config.get("debug"):
app_config["debug"] = True
self.__log_outside_reloader(logger, "Application is running in 'debug' mode")
- if app_config["debug"] and not app_config["allow_unsafe_werkzeug"]:
+ if app_config.get("debug") and not app_config.get("allow_unsafe_werkzeug"):
app_config["allow_unsafe_werkzeug"] = True
self.__log_outside_reloader(logger, "'allow_unsafe_werkzeug' has been set to True")
- if app_config["debug"] and app_config["async_mode"] != "threading":
+ if app_config.get("debug") and app_config.get("async_mode") != "threading":
app_config["async_mode"] = "threading"
self.__log_outside_reloader(
logger,
@@ -318,17 +320,15 @@ def resolve(self):
def _resolve_stylekit(self):
app_config = self.config
# support legacy margin variable
- stylekit_config = app_config["stylekit"]
+ stylekit_config = app_config.get("stylekit")
- if isinstance(app_config["stylekit"], dict) and "root_margin" in app_config["stylekit"]:
+ if isinstance(stylekit_config, dict) and "root_margin" in stylekit_config:
from ._default_config import _default_stylekit, default_config
- stylekit_config = app_config["stylekit"]
- if (
- stylekit_config["root_margin"] == _default_stylekit["root_margin"]
- and app_config["margin"] != default_config["margin"]
- ):
- app_config["stylekit"]["root_margin"] = str(app_config["margin"])
+ if stylekit_config.get("root_margin") == _default_stylekit.get("root_margin") and app_config.get(
+ "margin"
+ ) != default_config.get("margin"):
+ stylekit_config["root_margin"] = str(app_config.get("margin"))
app_config["margin"] = None
def _resolve_url_prefix(self):
@@ -343,4 +343,4 @@ def _resolve_url_prefix(self):
def _resolve_notebook_proxy(self):
app_config = self.config
- app_config["notebook_proxy"] = app_config["notebook_proxy"] if _is_in_notebook() else False
+ app_config["notebook_proxy"] = app_config.get("notebook_proxy", False) if _is_in_notebook() else False
diff --git a/taipy/gui/gui.py b/taipy/gui/gui.py
index 698eb58cc9..3dfbbdde37 100644
--- a/taipy/gui/gui.py
+++ b/taipy/gui/gui.py
@@ -470,7 +470,7 @@ def get_matplotlib_content(figure: MatplotlibFigure):
if callable(provider_fn):
try:
- return provider_fn(content)
+ return provider_fn(t.cast(t.Any, content))
except Exception as e:
_warn(f"Error in content provider for type {str(type(content))}", e)
return (
@@ -639,7 +639,7 @@ def _manage_message(self, msg_type: _WsType, message: dict) -> None:
self.__set_client_id_in_context(expected_client_id)
g.ws_client_id = expected_client_id
with self._set_locals_context(message.get("module_context") or None):
- with self._get_autorization():
+ with self._get_authorization():
payload = message.get("payload", {})
if msg_type == _WsType.UPDATE.value:
self.__front_end_update(
@@ -966,7 +966,7 @@ def _serve_status(self, template: Path) -> t.Dict[str, t.Dict[str, str]]:
def __upload_files(self):
self.__set_client_id_in_context()
on_upload_action = request.form.get("on_action", None)
- var_name = request.form.get("var_name", None)
+ var_name = t.cast(str, request.form.get("var_name", None))
if not var_name and not on_upload_action:
_warn("upload files: No var name")
return ("upload files: No var name", 400)
@@ -1057,7 +1057,7 @@ def __send_var_list_update( # noqa C901
else:
if isinstance(newvalue, (_TaipyContent, _TaipyContentImage)):
ret_value = self.__get_content_accessor().get_info(
- front_var, newvalue.get(), isinstance(newvalue, _TaipyContentImage)
+ t.cast(str, front_var), newvalue.get(), isinstance(newvalue, _TaipyContentImage)
)
if isinstance(ret_value, tuple):
newvalue = f"/{Gui.__CONTENT_ROOT}/{ret_value[0]}"
@@ -1160,8 +1160,8 @@ def __handle_ws_get_module_context(self, payload: t.Any):
page_path = Gui.__root_page_name
# Get Module Context
if mc := self._get_page_context(page_path):
- page_renderer = self._get_page(page_path)._renderer
- self._bind_custom_page_variables(page_renderer, self._get_client_id())
+ page_renderer = t.cast(_Page, self._get_page(page_path))._renderer
+ self._bind_custom_page_variables(t.cast(t.Any, page_renderer), self._get_client_id())
# get metadata if there is one
metadata: t.Dict[str, t.Any] = {}
if hasattr(page_renderer, "_metadata"):
@@ -1245,12 +1245,12 @@ def __handle_ws_app_id(self, message: t.Any):
def __handle_ws_get_routes(self):
routes = (
- [[self._config.root_page._route, self._config.root_page._renderer.page_type]]
+ [[self._config.root_page._route, t.cast(t.Any, self._config.root_page._renderer).page_type]]
if self._config.root_page
else []
)
routes += [
- [page._route, page._renderer.page_type]
+ [page._route, t.cast(t.Any, page._renderer).page_type]
for page in self._config.pages
if page._route != Gui.__root_page_name
]
@@ -1269,7 +1269,7 @@ def __send_ws(self, payload: dict, allow_grouping=True, send_back_only=False) ->
self._server._ws.emit(
"message",
payload,
- to=self.__get_ws_receiver(send_back_only),
+ to=t.cast(str, self.__get_ws_receiver(send_back_only)),
)
time.sleep(0.001)
except Exception as e: # pragma: no cover
@@ -1280,7 +1280,7 @@ def __send_ws(self, payload: dict, allow_grouping=True, send_back_only=False) ->
def __broadcast_ws(self, payload: dict, client_id: t.Optional[str] = None):
try:
to = list(self.__get_sids(client_id)) if client_id else []
- self._server._ws.emit("message", payload, to=to if to else None, include_self=True)
+ self._server._ws.emit("message", payload, to=t.cast(str, to) if to else None, include_self=True)
time.sleep(0.001)
except Exception as e: # pragma: no cover
_warn(f"Exception raised in WebSocket communication in '{self.__frame.f_code.co_name}'", e)
@@ -1291,7 +1291,7 @@ def __send_ack(self, ack_id: t.Optional[str]) -> None:
self._server._ws.emit(
"message",
{"type": _WsType.ACKNOWLEDGEMENT.value, "id": ack_id},
- to=self.__get_ws_receiver(True),
+ to=t.cast(str, self.__get_ws_receiver(True)),
)
time.sleep(0.001)
except Exception as e: # pragma: no cover
@@ -1493,7 +1493,7 @@ def __on_action(self, id: t.Optional[str], payload: t.Any) -> None:
def __call_function_with_args(self, **kwargs):
action_function = kwargs.get("action_function")
- id = kwargs.get("id")
+ id = t.cast(str, kwargs.get("id"))
payload = kwargs.get("payload")
if callable(action_function):
@@ -1501,7 +1501,7 @@ def __call_function_with_args(self, **kwargs):
argcount = action_function.__code__.co_argcount
if argcount > 0 and inspect.ismethod(action_function):
argcount -= 1
- args = [None for _ in range(argcount)]
+ args = t.cast(list, [None for _ in range(argcount)])
if argcount > 0:
args[0] = self.__get_state()
if argcount > 1:
@@ -1746,7 +1746,7 @@ def _tbl_cols(
attributes.get("date_format"),
attributes.get("number_format"),
)
- _enhance_columns(attributes, hashes, col_dict, "table(cols)")
+ _enhance_columns(attributes, hashes, t.cast(dict, col_dict), "table(cols)")
return json.dumps(col_dict, cls=_TaipyJsonEncoder)
except Exception as e: # pragma: no cover
@@ -1846,7 +1846,7 @@ def _get_locals_bind_from_context(self, context: t.Optional[str]) -> t.Dict[str,
def _get_locals_context(self) -> str:
current_context = self.__locals_context.get_context()
- return current_context if current_context is not None else self.__default_module_name
+ return current_context if current_context is not None else t.cast(str, self.__default_module_name)
def _set_locals_context(self, context: t.Optional[str]) -> t.ContextManager[None]:
return self.__locals_context.set_locals_context(context)
@@ -2409,7 +2409,7 @@ def get_flask_app(self) -> Flask:
The Flask instance used.
"""
if hasattr(self, "_server"):
- return self._server.get_flask()
+ return t.cast(Flask, self._server.get_flask())
raise RuntimeError("get_flask_app() cannot be invoked before run() has been called.")
def _set_frame(self, frame: t.Optional[FrameType]):
@@ -2486,21 +2486,21 @@ def __init_server(self):
self,
path_mapping=self._path_mapping,
flask=self._flask,
- async_mode=app_config["async_mode"],
- allow_upgrades=not app_config["notebook_proxy"],
+ async_mode=app_config.get("async_mode"),
+ allow_upgrades=not app_config.get("notebook_proxy"),
server_config=app_config.get("server_config"),
)
# Stop and reinitialize the server if it is still running as a thread
- if (_is_in_notebook() or app_config["run_in_thread"]) and hasattr(self._server, "_thread"):
+ if (_is_in_notebook() or app_config.get("run_in_thread")) and hasattr(self._server, "_thread"):
self.stop()
self._flask_blueprint = []
self._server = _Server(
self,
path_mapping=self._path_mapping,
flask=self._flask,
- async_mode=app_config["async_mode"],
- allow_upgrades=not app_config["notebook_proxy"],
+ async_mode=app_config.get("async_mode"),
+ allow_upgrades=not app_config.get("notebook_proxy"),
server_config=app_config.get("server_config"),
)
self._bindings()._new_scopes()
@@ -2509,16 +2509,16 @@ def __init_ngrok(self):
app_config = self._config.config
if hasattr(self, "_ngrok"):
# Keep the ngrok instance if token has not changed
- if app_config["ngrok_token"] == self._ngrok[1]:
+ if app_config.get("ngrok_token") == self._ngrok[1]:
_TaipyLogger._get_logger().info(f" * NGROK Public Url: {self._ngrok[0].public_url}")
return
# Close the old tunnel so new tunnel can open for new token
- ngrok.disconnect(self._ngrok[0].public_url)
- if app_config["run_server"] and (token := app_config["ngrok_token"]): # pragma: no cover
+ ngrok.disconnect(self._ngrok[0].public_url) # type: ignore[reportPossiblyUnboundVariable]
+ if app_config.get("run_server") and (token := app_config.get("ngrok_token")): # pragma: no cover
if not util.find_spec("pyngrok"):
raise RuntimeError("Cannot use ngrok as pyngrok package is not installed.")
- ngrok.set_auth_token(token)
- self._ngrok = (ngrok.connect(app_config["port"], "http"), token)
+ ngrok.set_auth_token(token) # type: ignore[reportPossiblyUnboundVariable]
+ self._ngrok = (ngrok.connect(app_config.get("port"), "http"), token) # type: ignore[reportPossiblyUnboundVariable]
_TaipyLogger._get_logger().info(f" * NGROK Public Url: {self._ngrok[0].public_url}")
def __bind_default_function(self):
@@ -2611,7 +2611,7 @@ def __register_blueprint(self):
# Register Flask Blueprint if available
for bp in self._flask_blueprint:
- self._server.get_flask().register_blueprint(bp)
+ t.cast(Flask, self._server.get_flask()).register_blueprint(bp)
def _get_accessor(self):
if self.__accessors is None:
@@ -2711,7 +2711,7 @@ def run(
locals_bind = _filter_locals(self.__frame.f_locals)
- self.__locals_context.set_default(locals_bind, self.__default_module_name)
+ self.__locals_context.set_default(locals_bind, t.cast(str, self.__default_module_name))
self.__var_dir.set_default(self.__frame)
@@ -2761,25 +2761,27 @@ def run(
self.__register_blueprint()
# Register data accessor communication data format (JSON, Apache Arrow)
- self._get_accessor().set_data_format(_DataFormat.APACHE_ARROW if app_config["use_arrow"] else _DataFormat.JSON)
+ self._get_accessor().set_data_format(
+ _DataFormat.APACHE_ARROW if app_config.get("use_arrow") else _DataFormat.JSON
+ )
# Use multi user or not
- self._bindings()._set_single_client(bool(app_config["single_client"]))
+ self._bindings()._set_single_client(bool(app_config.get("single_client")))
# Start Flask Server
if not run_server:
return self.get_flask_app()
return self._server.run(
- host=app_config["host"],
- port=app_config["port"],
- debug=app_config["debug"],
- use_reloader=app_config["use_reloader"],
- flask_log=app_config["flask_log"],
- run_in_thread=app_config["run_in_thread"],
- allow_unsafe_werkzeug=app_config["allow_unsafe_werkzeug"],
- notebook_proxy=app_config["notebook_proxy"],
- port_auto_ranges=app_config["port_auto_ranges"],
+ host=app_config.get("host"),
+ port=app_config.get("port"),
+ debug=app_config.get("debug"),
+ use_reloader=app_config.get("use_reloader"),
+ flask_log=app_config.get("flask_log"),
+ run_in_thread=app_config.get("run_in_thread"),
+ allow_unsafe_werkzeug=app_config.get("allow_unsafe_werkzeug"),
+ notebook_proxy=app_config.get("notebook_proxy"),
+ port_auto_ranges=app_config.get("port_auto_ranges"),
)
def reload(self): # pragma: no cover
@@ -2807,7 +2809,7 @@ def stop(self):
self._server.stop_thread()
_TaipyLogger._get_logger().info("Gui server has been stopped.")
- def _get_autorization(self, client_id: t.Optional[str] = None, system: t.Optional[bool] = False):
+ def _get_authorization(self, client_id: t.Optional[str] = None, system: t.Optional[bool] = False):
return contextlib.nullcontext()
def set_favicon(self, favicon_path: t.Union[str, Path], state: t.Optional[State] = None):
diff --git a/taipy/gui/partial.py b/taipy/gui/partial.py
index 47a5d22097..54d5ef7318 100644
--- a/taipy/gui/partial.py
+++ b/taipy/gui/partial.py
@@ -53,7 +53,7 @@ def __init__(self, route: t.Optional[str] = None):
else:
self._route = route
- def update_content(self, state: State, content: str | "Page"):
+ def update_content(self, state: State, content: t.Union[str, "Page"]):
"""Update partial content.
Arguments:
@@ -65,7 +65,7 @@ def update_content(self, state: State, content: str | "Page"):
else:
_warn("'Partial.update_content()' must be called in the context of a callback.")
- def __copy(self, content: str | "Page") -> Partial:
+ def __copy(self, content: t.Union[str, "Page"]) -> Partial:
new_partial = Partial(self._route)
from .page import Page
diff --git a/taipy/gui/server.py b/taipy/gui/server.py
index ab62ed02bc..dfada6091f 100644
--- a/taipy/gui/server.py
+++ b/taipy/gui/server.py
@@ -247,14 +247,14 @@ def get_flask(self):
return self._flask
def test_client(self):
- return self._flask.test_client()
+ return t.cast(Flask, self._flask).test_client()
def _run_notebook(self):
self._is_running = True
self._ws.run(self._flask, host=self._host, port=self._port, debug=False, use_reloader=False)
def _get_async_mode(self) -> str:
- return self._ws.async_mode
+ return self._ws.async_mode # type: ignore[reportAttributeAccessIssue]
def _apply_patch(self):
if self._get_async_mode() == "gevent" and util.find_spec("gevent"):
@@ -264,7 +264,7 @@ def _apply_patch(self):
if not monkey.is_module_patched("time"):
monkey.patch_time()
if self._get_async_mode() == "eventlet" and util.find_spec("eventlet"):
- from eventlet import monkey_patch, patcher
+ from eventlet import monkey_patch, patcher # type: ignore[reportMissingImport]
if not patcher.is_monkey_patched("time"):
monkey_patch(time=True)
@@ -349,8 +349,8 @@ def stop_thread(self):
self._is_running = False
with contextlib.suppress(Exception):
if self._get_async_mode() == "gevent":
- if self._ws.wsgi_server is not None:
- self._ws.wsgi_server.stop()
+ if self._ws.wsgi_server is not None: # type: ignore[reportAttributeAccessIssue]
+ self._ws.wsgi_server.stop() # type: ignore[reportAttributeAccessIssue]
else:
self._thread.kill()
else:
diff --git a/taipy/gui/utils/_adapter.py b/taipy/gui/utils/_adapter.py
index 58f97510a2..4aa944dd7c 100644
--- a/taipy/gui/utils/_adapter.py
+++ b/taipy/gui/utils/_adapter.py
@@ -137,7 +137,7 @@ def _run(
return (
add(type(result)(tpl_res), result[len(tpl_res) :])
if isinstance(result, (tuple, list)) and isinstance(tpl_res, (tuple, list))
- else tpl_res
+ else tpl_res # type: ignore[reportReturnType]
)
except Exception as e:
_warn(f"Cannot run adapter for {var_name}", e)
@@ -168,9 +168,9 @@ def __get_id(self, value: t.Any, dig=True) -> str:
if isinstance(value, (list, tuple)) and len(value):
return self.__get_id(value[0], False)
elif hasattr(value, "id"):
- return self.__get_id(value.id, False)
+ return self.__get_id(t.cast(t.Any, value).id, False)
elif hasattr(value, "__getitem__") and "id" in value:
- return self.__get_id(value.get("id"), False)
+ return self.__get_id(t.cast(dict, value).get("id"), False)
if value is not None and type(value).__name__ not in self.__warning_by_type:
_warn(f"LoV id must be a string, using a string representation of {type(value)}.")
self.__warning_by_type.add(type(value).__name__)
@@ -183,9 +183,9 @@ def __get_label(self, value: t.Any, dig=True) -> t.Union[str, t.Dict, None]:
if isinstance(value, (list, tuple)) and len(value) > 1:
return self.__get_label(value[1], False)
elif hasattr(value, "label"):
- return self.__get_label(value.label, False)
+ return self.__get_label(t.cast(t.Any, value).label, False)
elif hasattr(value, "__getitem__") and "label" in value:
- return self.__get_label(value["label"], False)
+ return self.__get_label(t.cast(dict, value).get("label"), False)
return None
def __get_children(self, value: t.Any) -> t.Optional[t.List[t.Any]]:
@@ -193,18 +193,18 @@ def __get_children(self, value: t.Any) -> t.Optional[t.List[t.Any]]:
return value[2] if isinstance(value[2], list) else None if value[2] is None else [value[2]]
elif hasattr(value, "children"):
return (
- value.children
- if isinstance(value.children, list)
+ t.cast(t.Any, value).children
+ if isinstance(t.cast(t.Any, value).children, list)
else None
- if value.children is None
- else [value.children]
+ if t.cast(t.Any, value).children is None
+ else [t.cast(t.Any, value).children]
)
elif hasattr(value, "__getitem__") and "children" in value:
return (
- value["children"]
- if isinstance(value["children"], list)
+ t.cast(dict, value).get("children")
+ if isinstance(t.cast(dict, value).get("children"), list)
else None
- if value["children"] is None
- else [value["children"]]
+ if t.cast(dict, value).get("children") is None
+ else [t.cast(dict, value).get("children")]
)
return None
diff --git a/taipy/gui/utils/_bindings.py b/taipy/gui/utils/_bindings.py
index 2139888dac..6189245ccb 100644
--- a/taipy/gui/utils/_bindings.py
+++ b/taipy/gui/utils/_bindings.py
@@ -40,7 +40,7 @@ def _bind(self, name: str, value: t.Any) -> None:
def __get_property(self, name):
def __setter(ud: _Bindings, value: t.Any):
if isinstance(value, _MapDict):
- value._update_var = None
+ value._update_var = None # type: ignore[assignment]
elif isinstance(value, dict):
value = _MapDict(value, None)
ud.__gui._update_var(name, value)
diff --git a/taipy/gui/utils/_evaluator.py b/taipy/gui/utils/_evaluator.py
index bcafc8547b..a5aa3c83a7 100644
--- a/taipy/gui/utils/_evaluator.py
+++ b/taipy/gui/utils/_evaluator.py
@@ -242,7 +242,7 @@ def evaluate_expr(
ctx.update(self.__global_ctx)
# entries in var_val are not always seen (NameError) when passed as locals
ctx.update(var_val)
- with gui._get_autorization():
+ with gui._get_authorization():
expr_evaluated = eval(not_encoded_expr if is_edge_case else expr_string, ctx)
except Exception as e:
_warn(f"Cannot evaluate expression '{not_encoded_expr if is_edge_case else expr_string}'", e)
diff --git a/taipy/gui/utils/_map_dict.py b/taipy/gui/utils/_map_dict.py
index c6ba4db0a0..8c1cae7d10 100644
--- a/taipy/gui/utils/_map_dict.py
+++ b/taipy/gui/utils/_map_dict.py
@@ -22,16 +22,16 @@ class _MapDict(object):
__local_vars = ("_dict", "_update_var")
- def __init__(self, dict_import: dict, app_update_var=None):
+ def __init__(self, dict_import: dict, app_update_var: t.Optional[t.Callable]=None):
self._dict = dict_import
# Bind app update var function
- self._update_var = app_update_var
+ self._update_var = t.cast(t.Callable, app_update_var)
def __len__(self):
return self._dict.__len__()
def __length_hint__(self):
- return self._dict.__length_hint__()
+ return self._dict.__length_hint__() # type: ignore[reportAttributeAccessIssue]
def __getitem__(self, key):
value = self._dict.__getitem__(key)
@@ -52,7 +52,7 @@ def __delitem__(self, key):
self._dict.__delitem__(key)
def __missing__(self, key):
- return self._dict.__missing__(key)
+ return self._dict.__missing__(key) # type: ignore[reportAttributeAccessIssue]
def __iter__(self):
return self._dict.__iter__()
diff --git a/taipy/gui/utils/_runtime_manager.py b/taipy/gui/utils/_runtime_manager.py
index 44877ef706..383afb1c57 100644
--- a/taipy/gui/utils/_runtime_manager.py
+++ b/taipy/gui/utils/_runtime_manager.py
@@ -22,8 +22,8 @@ def __init__(self) -> None:
self.__port_gui: t.Dict[int, "Gui"] = {}
def add_gui(self, gui: "Gui", port: int):
- if port in self.__port_gui:
- self.__port_gui[port].stop()
+ if gui_port := self.__port_gui.get(port):
+ gui_port.stop()
self.__port_gui[port] = gui
def get_used_port(self):
diff --git a/taipy/gui/utils/_variable_directory.py b/taipy/gui/utils/_variable_directory.py
index ad38e7c6bb..4d58442681 100644
--- a/taipy/gui/utils/_variable_directory.py
+++ b/taipy/gui/utils/_variable_directory.py
@@ -36,7 +36,7 @@ def add_frame(self, frame: t.Optional[FrameType]) -> None:
module_name = _get_module_name_from_frame(frame)
if module_name not in self._imported_var_dir:
imported_var_list = _get_imported_var(frame)
- self._imported_var_dir[module_name] = imported_var_list
+ self._imported_var_dir[t.cast(str, module_name)] = imported_var_list
def pre_process_module_import_all(self) -> None:
for imported_dir in self._imported_var_dir.values():
@@ -54,7 +54,7 @@ def pre_process_module_import_all(self) -> None:
def process_imported_var(self) -> None:
self.pre_process_module_import_all()
- default_imported_dir = self._imported_var_dir[self._default_module]
+ default_imported_dir = self._imported_var_dir[t.cast(str, self._default_module)]
with self._locals_context.set_locals_context(self._default_module):
for name, asname, module in default_imported_dir:
if name == "*" and asname == "*":
@@ -85,7 +85,7 @@ def process_imported_var(self) -> None:
def add_var(self, name: str, module: t.Optional[str], var_name: t.Optional[str] = None) -> str:
if module is None:
- module = self._default_module
+ module = t.cast(str, self._default_module)
if gv := self.get_var(name, module):
return gv
var_encode = _variable_encode(name, module) if module != self._default_module else name
@@ -95,7 +95,7 @@ def add_var(self, name: str, module: t.Optional[str], var_name: t.Optional[str]
if var_encode != var_name:
var_name_decode, module_decode = _variable_decode(var_name)
if module_decode is None:
- module_decode = self._default_module
+ module_decode = t.cast(str, self._default_module)
self.__add_var_head(var_name_decode, module_decode, var_encode)
if name not in self._var_dir:
self._var_dir[name] = {module: var_name}
diff --git a/taipy/gui/utils/chart_config_builder.py b/taipy/gui/utils/chart_config_builder.py
index 8d871137e8..12e4942aeb 100644
--- a/taipy/gui/utils/chart_config_builder.py
+++ b/taipy/gui/utils/chart_config_builder.py
@@ -207,7 +207,10 @@ def _build_chart_config(gui: "Gui", attributes: t.Dict[str, t.Any], col_types: t
decimators.append(None)
# set default columns if not defined
- icols = [[c2 for c2 in [__get_col_from_indexed(c1, i) for c1 in col_dict.keys()] if c2] for i in range(len(traces))]
+ icols = [
+ [c2 for c2 in [__get_col_from_indexed(c1, i) for c1 in t.cast(dict, col_dict).keys()] if c2]
+ for i in range(len(traces))
+ ]
for i, tr in enumerate(traces):
if i < len(axis):
diff --git a/taipy/gui/utils/datatype.py b/taipy/gui/utils/datatype.py
index 197bf6fadf..f6b89bad0f 100644
--- a/taipy/gui/utils/datatype.py
+++ b/taipy/gui/utils/datatype.py
@@ -22,4 +22,4 @@ def _get_data_type(value):
return "int"
elif pd.api.types.is_float_dtype(value):
return "float"
- return re.match(r"^", str(type(value))).group(2)
+ return re.match(r"^", str(type(value))).group(2) # type: ignore[reportOptionalMemberAccess]
diff --git a/taipy/gui/utils/isnotebook.py b/taipy/gui/utils/isnotebook.py
index c9ef17a08a..05f9d72937 100644
--- a/taipy/gui/utils/isnotebook.py
+++ b/taipy/gui/utils/isnotebook.py
@@ -17,7 +17,7 @@ def _is_in_notebook(): # pragma: no cover
if not util.find_spec("IPython"):
return False
- from IPython import get_ipython
+ from IPython import get_ipython # type: ignore[reportPrivateImportUsage]
ipython = get_ipython()
diff --git a/taipy/gui/viselements.json b/taipy/gui/viselements.json
index 3135e38bd5..d1b6440649 100644
--- a/taipy/gui/viselements.json
+++ b/taipy/gui/viselements.json
@@ -1656,6 +1656,12 @@
"type": "bool",
"default_value": "False",
"doc": "If True, the sender avatar and name are displayed."
+ },
+ {
+ "name": "mode",
+ "type": "str",
+ "default_value": "\"markdown\"",
+ "doc": "Define the way the messages are processed:\n- "raw" no processing
- "pre": keeps spaces and new lines
- "markdown" or "md": basic support for Markdown.
"
}
]
}
diff --git a/taipy/gui_core/__init__.py b/taipy/gui_core/__init__.py
index 61ef2419e0..68565af06f 100644
--- a/taipy/gui_core/__init__.py
+++ b/taipy/gui_core/__init__.py
@@ -14,5 +14,5 @@
This package provides classes that can be used in GUI controls dedicated to scenario management.
"""
-from ._adapters import CustomScenarioFilter, DataNodeFilter, DataNodeScenarioFilter, ScenarioFilter
from ._init import *
+from .filters import CustomScenarioFilter, DataNodeFilter, DataNodeScenarioFilter, ScenarioFilter
diff --git a/taipy/gui_core/_adapters.py b/taipy/gui_core/_adapters.py
index b48a2ceb4f..e28d0327ae 100644
--- a/taipy/gui_core/_adapters.py
+++ b/taipy/gui_core/_adapters.py
@@ -43,11 +43,13 @@
from taipy.gui.gui import _DoNotUpdate
from taipy.gui.utils import _is_boolean, _is_true, _TaipyBase
+from .filters import DataNodeFilter, ScenarioFilter, _Filter
+
# prevent gui from trying to push scenario instances to the front-end
class _GuiCoreDoNotUpdate(_DoNotUpdate):
def __repr__(self):
- return self.get_label() if hasattr(self, "get_label") else super().__repr__()
+ return self.get_label() if hasattr(self, "get_label") else super().__repr__() # type: ignore[reportAttributeAccessIssue]
class _EntityType(Enum):
@@ -353,18 +355,18 @@ def sort_key(entity: t.Union[Scenario, Cycle, Sequence, DataNode]):
# we compare only strings
if isinstance(entity, types):
if isinstance(entity, Cycle):
- lcol = "creation_date"
- lfn = None
+ the_col = "creation_date"
+ the_fn = None
else:
- lcol = col
- lfn = col_fn
+ the_col = col
+ the_fn = col_fn
try:
- val = attrgetter(lfn or lcol)(entity)
- if lfn:
+ val = attrgetter(the_fn or the_col)(entity)
+ if the_fn:
val = val()
except AttributeError as e:
if _is_debugging():
- _warn("Attribute", e)
+ _warn(f"sort_key({entity.id}):", e)
val = ""
else:
val = ""
@@ -373,76 +375,17 @@ def sort_key(entity: t.Union[Scenario, Cycle, Sequence, DataNode]):
return sort_key
-@dataclass
-class _Filter(_DoNotUpdate):
- label: str
- property_type: t.Optional[t.Type]
-
- def get_property(self):
- return self.label
-
- def get_type(self):
- if self.property_type is bool:
- return "boolean"
- elif self.property_type is int or self.property_type is float:
- return "number"
- elif self.property_type is datetime or self.property_type is date:
- return "date"
- elif self.property_type is str:
- return "str"
- return "any"
-
-
-@dataclass
-class ScenarioFilter(_Filter):
- property_id: str
-
- def get_property(self):
- return self.property_id
-
-
-@dataclass
-class DataNodeScenarioFilter(_Filter):
- datanode_config_id: str
- property_id: str
-
- def get_property(self):
- return f"{self.datanode_config_id}.{self.property_id}"
-
-
-_CUSTOM_PREFIX = "fn:"
-
-
-@dataclass
-class CustomScenarioFilter(_Filter):
- filter_function: t.Callable[[Scenario], t.Any]
-
- def __post_init__(self):
- if self.filter_function.__name__ == "":
- raise TypeError("ScenarioCustomFilter does not support lambda functions.")
- mod = self.filter_function.__module__
- self.module = mod if isinstance(mod, str) else mod.__name__
-
- def get_property(self):
- return f"{_CUSTOM_PREFIX}{self.module}:{self.filter_function.__name__}"
-
- @staticmethod
- def _get_custom(col: str) -> t.Optional[t.List[str]]:
- return col[len(_CUSTOM_PREFIX) :].split(":") if col.startswith(_CUSTOM_PREFIX) else None
-
-
-@dataclass
-class DataNodeFilter(_Filter):
- property_id: str
-
- def get_property(self):
- return self.property_id
+@dataclass(frozen=True)
+class _GuiCorePropDesc:
+ filter: _Filter
+ extended: bool = False
+ for_sort: bool = False
class _GuiCoreProperties(ABC):
@staticmethod
@abstractmethod
- def get_default_list() -> t.List[_Filter]:
+ def get_default_list() -> t.List[_GuiCorePropDesc]:
raise NotImplementedError
@staticmethod
@@ -454,7 +397,7 @@ def get_enums(self):
return {}
def get(self):
- data = super().get()
+ data = super().get() # type: ignore[reportAttributeAccessIssue]
if _is_boolean(data):
if _is_true(data):
data = self.get_default_list()
@@ -465,18 +408,21 @@ def get(self):
if isinstance(data, _Filter):
data = (data,)
if isinstance(data, (list, tuple)):
- flist: t.List[_Filter] = [] # type: ignore[annotation-unchecked]
+ f_list: t.List[_Filter] = [] # type: ignore[annotation-unchecked]
for f in data:
if isinstance(f, str):
f = f.strip()
if f == "*":
- flist.extend(p.filter for p in self.get_default_list())
+ f_list.extend(p.filter for p in self.get_default_list())
elif f:
- flist.append(
- next((p.filter for p in self.get_default_list() if p.get_property() == f), _Filter(f))
+ f_list.append(
+ next(
+ (p.filter for p in self.get_default_list() if p.filter.get_property() == f),
+ _Filter(f, None),
+ )
)
elif isinstance(f, _Filter):
- flist.append(f)
+ f_list.append(f)
return json.dumps(
[
(
@@ -487,19 +433,12 @@ def get(self):
)
if self.full_desc()
else (attr.label, attr.get_property())
- for attr in flist
+ for attr in f_list
]
)
return None
-@dataclass(frozen=True)
-class _GuiCorePropDesc:
- filter: _Filter
- extended: bool = False
- for_sort: bool = False
-
-
class _GuiCoreScenarioProperties(_GuiCoreProperties):
_SC_PROPS: t.List[_GuiCorePropDesc] = [
_GuiCorePropDesc(ScenarioFilter("Config id", str, "config_id"), for_sort=True),
diff --git a/taipy/gui_core/_context.py b/taipy/gui_core/_context.py
index 53f8bd8e59..2c40a3854a 100644
--- a/taipy/gui_core/_context.py
+++ b/taipy/gui_core/_context.py
@@ -67,13 +67,13 @@
from taipy.gui.utils._map_dict import _MapDict
from ._adapters import (
- CustomScenarioFilter,
_EntityType,
_get_entity_property,
_GuiCoreDatanodeAdapter,
_GuiCoreScenarioProperties,
_invoke_action,
)
+from .filters import CustomScenarioFilter
class _GuiCoreContext(CoreEventConsumerBase):
@@ -113,20 +113,20 @@ def __lazy_start(self):
def process_event(self, event: Event):
self.__lazy_start()
- if event.entity_type == EventEntityType.SCENARIO:
- with self.gui._get_autorization(system=True):
+ if event.entity_type is EventEntityType.SCENARIO:
+ with self.gui._get_authorization(system=True):
self.scenario_refresh(
event.entity_id
- if event.operation == EventOperation.DELETION or is_readable(t.cast(ScenarioId, event.entity_id))
+ if event.operation is EventOperation.DELETION or is_readable(t.cast(ScenarioId, event.entity_id))
else None
)
- elif event.entity_type == EventEntityType.SEQUENCE and event.entity_id:
+ elif event.entity_type is EventEntityType.SEQUENCE and event.entity_id:
sequence = None
try:
- with self.gui._get_autorization(system=True):
+ with self.gui._get_authorization(system=True):
sequence = (
core_get(event.entity_id)
- if event.operation != EventOperation.DELETION
+ if event.operation is not EventOperation.DELETION
and is_readable(t.cast(SequenceId, event.entity_id))
else None
)
@@ -134,13 +134,15 @@ def process_event(self, event: Event):
self.broadcast_core_changed({"scenario": list(sequence.parent_ids)}) # type: ignore
except Exception as e:
_warn(f"Access to sequence {event.entity_id} failed", e)
- elif event.entity_type == EventEntityType.JOB:
+ elif event.entity_type is EventEntityType.JOB:
with self.lock:
self.jobs_list = None
- self.broadcast_core_changed({"jobs": event.entity_id})
- elif event.entity_type == EventEntityType.SUBMISSION:
+ # no broadcast because the submission status will do the job
+ if event.operation is EventOperation.DELETION:
+ self.broadcast_core_changed({"jobs": True})
+ elif event.entity_type is EventEntityType.SUBMISSION:
self.submission_status_callback(event.entity_id, event)
- elif event.entity_type == EventEntityType.DATA_NODE:
+ elif event.entity_type is EventEntityType.DATA_NODE:
with self.lock:
self.data_nodes_by_owner = None
self.broadcast_core_changed(
@@ -178,9 +180,9 @@ def submission_status_callback(self, submission_id: t.Optional[str] = None, even
client_id = submission.properties.get("client_id")
if client_id:
running_tasks = {}
- with self.gui._get_autorization(client_id):
+ with self.gui._get_authorization(client_id):
for job in submission.jobs:
- job = job if isinstance(job, Job) else core_get(job)
+ job = job if isinstance(job, Job) else t.cast(Job, core_get(job))
running_tasks[job.task.id] = (
SubmissionStatus.RUNNING.value
if job.is_running()
@@ -190,7 +192,7 @@ def submission_status_callback(self, submission_id: t.Optional[str] = None, even
)
payload.update(tasks=running_tasks)
- if last_status != new_status:
+ if last_status is not new_status:
# callback
submission_name = submission.properties.get("on_submission")
if submission_name:
@@ -308,7 +310,7 @@ def get_sorted_scenario_list(
def get_filtered_scenario_list(
self,
- entities: t.List[t.Union[t.List, Scenario]],
+ entities: t.List[t.Union[t.List, Scenario, None]],
filters: t.Optional[t.List[t.Dict[str, t.Any]]],
):
if not filters:
@@ -340,13 +342,15 @@ def get_filtered_scenario_list(
e
for e in filtered_list
if not isinstance(e, Scenario)
- or _invoke_action(e, col, col_type, is_datanode_prop, action, val, col_fn)
+ or _invoke_action(e, t.cast(str, col), col_type, is_datanode_prop, action, val, col_fn)
]
# level 2 filtering
filtered_list = [
e
if isinstance(e, Scenario)
- else self.filter_entities(e, col, col_type, is_datanode_prop, action, val, col_fn)
+ else self.filter_entities(
+ t.cast(list, e), t.cast(str, col), col_type, is_datanode_prop, action, val, col_fn
+ )
for e in filtered_list
]
# remove empty cycles
@@ -414,17 +418,17 @@ def crud_scenario(self, state: State, id: str, payload: t.Dict[str, str]): # no
or not isinstance(args[start_idx + 2], dict)
):
return
- error_var = payload.get("error_id")
+ error_var = t.cast(str, payload.get("error_id"))
update = args[start_idx]
delete = args[start_idx + 1]
- data = args[start_idx + 2]
+ data = t.cast(dict, args[start_idx + 2])
with_dialog = True if len(args) < start_idx + 4 else bool(args[start_idx + 3])
scenario = None
user_scenario = None
name = data.get(_GuiCoreContext.__PROP_ENTITY_NAME)
if update:
- scenario_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
+ scenario_id = t.cast(ScenarioId, data.get(_GuiCoreContext.__PROP_ENTITY_ID))
if delete:
if not (reason := is_deletable(scenario_id)):
state.assign(error_var, f"Scenario. {scenario_id} is not deletable: {_get_reason(reason)}.")
@@ -454,11 +458,11 @@ def crud_scenario(self, state: State, id: str, payload: t.Dict[str, str]): # no
scenario_config = None
date = None
scenario_id = None
+ gui = state.get_gui()
try:
- gui = state.get_gui()
on_creation = args[0] if isinstance(args[0], str) else None
on_creation_function = gui._get_user_function(on_creation) if on_creation else None
- if callable(on_creation_function):
+ if callable(on_creation_function) and on_creation:
try:
res = gui._call_function_with_state(
on_creation_function,
@@ -503,9 +507,8 @@ def crud_scenario(self, state: State, id: str, payload: t.Dict[str, str]): # no
+ f"({len(Config.scenarios) - 1}) found.",
)
return
-
- scenario = create_scenario(scenario_config, date, name)
- scenario_id = scenario.id
+ scenario = create_scenario(scenario_config, date, name) if scenario_config else None
+ scenario_id = scenario.id if scenario else None
except Exception as e:
state.assign(error_var, f"Error creating Scenario. {e}")
finally:
@@ -547,17 +550,17 @@ def edit_entity(self, state: State, id: str, payload: t.Dict[str, str]):
if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
return
error_var = payload.get("error_id")
- data = args[0]
- entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
+ data = t.cast(dict, args[0])
+ entity_id = t.cast(str, data.get(_GuiCoreContext.__PROP_ENTITY_ID))
sequence = data.get("sequence")
if not self.__check_readable_editable(state, entity_id, "Scenario", error_var):
return
- scenario: Scenario = core_get(entity_id)
+ scenario = t.cast(Scenario, core_get(entity_id))
if scenario:
try:
if not sequence:
if isinstance(sequence, str) and (name := data.get(_GuiCoreContext.__PROP_ENTITY_NAME)):
- scenario.add_sequence(name, data.get("task_ids"))
+ scenario.add_sequence(name, t.cast(list, data.get("task_ids")))
else:
primary = data.get(_GuiCoreContext.__PROP_SCENARIO_PRIMARY)
if primary is True:
@@ -572,11 +575,11 @@ def edit_entity(self, state: State, id: str, payload: t.Dict[str, str]):
if data.get("del", False):
scenario.remove_sequence(sequence)
else:
- name = data.get(_GuiCoreContext.__PROP_ENTITY_NAME)
+ name = t.cast(str, data.get(_GuiCoreContext.__PROP_ENTITY_NAME))
if sequence != name:
scenario.rename_sequence(sequence, name)
if seqEntity := scenario.sequences.get(name):
- seqEntity.tasks = data.get("task_ids")
+ seqEntity.tasks = t.cast(list, data.get("task_ids"))
self.__edit_properties(seqEntity, data)
else:
_GuiCoreContext.__assign_var(
@@ -595,13 +598,13 @@ def submit_entity(self, state: State, id: str, payload: t.Dict[str, str]):
args = payload.get("args")
if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
return
- data = args[0]
+ data = t.cast(dict, args[0])
error_var = payload.get("error_id")
try:
- scenario_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
- entity = core_get(scenario_id)
- if sequence := data.get("sequence"):
- entity = entity.sequences.get(sequence)
+ scenario_id = t.cast(str, data.get(_GuiCoreContext.__PROP_ENTITY_ID))
+ entity = t.cast(Scenario, core_get(scenario_id))
+ if sequence := t.cast(str, data.get("sequence")):
+ entity = t.cast(Sequence, entity.sequences.get(sequence))
if not (reason := is_submittable(entity)):
_GuiCoreContext.__assign_var(
@@ -631,7 +634,7 @@ def submit_entity(self, state: State, id: str, payload: t.Dict[str, str]):
def get_filtered_datanode_list(
self,
- entities: t.List[t.Union[t.List, DataNode]],
+ entities: t.List[t.Union[t.List, DataNode, None]],
filters: t.Optional[t.List[t.Dict[str, t.Any]]],
):
if not filters or not entities:
@@ -660,13 +663,16 @@ def get_filtered_datanode_list(
filtered_list = [
e
for e in filtered_list
- if not isinstance(e, DataNode) or _invoke_action(e, col, col_type, False, action, val, col_fn)
+ if not isinstance(e, DataNode)
+ or _invoke_action(e, t.cast(str, col), col_type, False, action, val, col_fn)
]
# level 3 filtering
filtered_list = [
- e if isinstance(e, DataNode) else self.filter_entities(d, col, col_type, False, action, val, col_fn)
+ e
+ if isinstance(e, DataNode)
+ else self.filter_entities(d, t.cast(str, col), col_type, False, action, val, col_fn)
for e in filtered_list
- for d in e[2]
+ for d in t.cast(list, t.cast(list, e)[2])
]
# remove empty cycles
return [e for e in filtered_list if isinstance(e, DataNode) or (isinstance(e, (tuple, list)) and len(e[2]))]
@@ -724,8 +730,8 @@ def get_datanodes_tree(
base_list = []
else:
base_list = datanodes
- adapted_list = self.get_sorted_datanode_list(base_list, sorts)
- return self.get_filtered_datanode_list(adapted_list, filters)
+ adapted_list = self.get_sorted_datanode_list(t.cast(list, base_list), sorts)
+ return self.get_filtered_datanode_list(t.cast(list, adapted_list), filters)
def data_node_adapter(
self,
@@ -737,8 +743,8 @@ def data_node_adapter(
if isinstance(data, tuple):
raise NotImplementedError
if isinstance(data, list):
- if data[2] and isinstance(data[2][0], (Cycle, Scenario, Sequence, DataNode)):
- data[2] = self.get_sorted_datanode_list(data[2], sorts, False)
+ if data[2] and isinstance(t.cast(list, data[2])[0], (Cycle, Scenario, Sequence, DataNode)):
+ data[2] = self.get_sorted_datanode_list(t.cast(list, data[2]), sorts, False)
return data
try:
if hasattr(data, "id") and is_readable(data.id) and core_get(data.id) is not None:
@@ -770,7 +776,7 @@ def data_node_adapter(
data.id,
data.get_simple_label(),
self.get_sorted_datanode_list(
- self.data_nodes_by_owner.get(data.id, []) + list(data.sequences.values()),
+ t.cast(list, self.data_nodes_by_owner.get(data.id, []) + list(data.sequences.values())),
sorts,
False,
),
@@ -828,7 +834,7 @@ def act_on_jobs(self, state: State, id: str, payload: t.Dict[str, str]):
args = payload.get("args")
if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
return
- data = args[0]
+ data = t.cast(dict, args[0])
job_ids = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
job_action = data.get(_GuiCoreContext.__ACTION)
if job_action and isinstance(job_ids, list):
@@ -879,7 +885,7 @@ def get_job_details(self, job_id: t.Optional[JobId]):
[] if job.stacktrace is None else job.stacktrace,
)
except Exception as e:
- _warn(f"Access to job ({job.id if hasattr(job, 'id') else 'No_id'}) failed", e)
+ _warn(f"Access to job ({job_id}) failed", e)
return None
def edit_data_node(self, state: State, id: str, payload: t.Dict[str, str]):
@@ -888,11 +894,11 @@ def edit_data_node(self, state: State, id: str, payload: t.Dict[str, str]):
if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
return
error_var = payload.get("error_id")
- data = args[0]
- entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
+ data = t.cast(dict, args[0])
+ entity_id = t.cast(str, data.get(_GuiCoreContext.__PROP_ENTITY_ID))
if not self.__check_readable_editable(state, entity_id, "DataNode", error_var):
return
- entity: DataNode = core_get(entity_id)
+ entity = t.cast(DataNode, core_get(entity_id))
if isinstance(entity, DataNode):
try:
self.__edit_properties(entity, data)
@@ -905,13 +911,13 @@ def lock_datanode_for_edit(self, state: State, id: str, payload: t.Dict[str, str
args = payload.get("args")
if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
return
- data = args[0]
+ data = t.cast(dict, args[0])
error_var = payload.get("error_id")
- entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
+ entity_id = t.cast(str, data.get(_GuiCoreContext.__PROP_ENTITY_ID))
if not self.__check_readable_editable(state, entity_id, "Data node", error_var):
return
lock = data.get("lock", True)
- entity: DataNode = core_get(entity_id)
+ entity = t.cast(DataNode, core_get(entity_id))
if isinstance(entity, DataNode):
try:
if lock:
@@ -937,13 +943,13 @@ def __edit_properties(self, entity: t.Union[Scenario, Sequence, DataNode], data:
props = data.get("properties")
if isinstance(props, (list, tuple)):
for prop in props:
- key = prop.get("key")
+ key = t.cast(dict, prop).get("key")
if key and key not in _GuiCoreContext.__ENTITY_PROPS:
- ent.properties[key] = prop.get("value")
+ ent.properties[key] = t.cast(dict, prop).get("value")
deleted_props = data.get("deleted_properties")
if isinstance(deleted_props, (list, tuple)):
for prop in deleted_props:
- key = prop.get("key")
+ key = t.cast(dict, prop).get("key")
if key and key not in _GuiCoreContext.__ENTITY_PROPS:
ent.properties.pop(key, None)
@@ -1006,23 +1012,24 @@ def update_data(self, state: State, id: str, payload: t.Dict[str, str]):
args = payload.get("args")
if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
return
- data = args[0]
+ data = t.cast(dict, args[0])
error_var = payload.get("error_id")
- entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
+ entity_id = t.cast(str, data.get(_GuiCoreContext.__PROP_ENTITY_ID))
if not self.__check_readable_editable(state, entity_id, "Data node", error_var):
return
- entity: DataNode = core_get(entity_id)
+ entity = t.cast(DataNode, core_get(entity_id))
if isinstance(entity, DataNode):
try:
+ val = t.cast(str, data.get("value"))
entity.write(
- parser.parse(data.get("value"))
+ parser.parse(val)
if data.get("type") == "date"
- else int(data.get("value"))
+ else int(val)
if data.get("type") == "int"
- else float(data.get("value"))
+ else float(val)
if data.get("type") == "float"
else data.get("value"),
- comment=data.get(_GuiCoreContext.__PROP_ENTITY_COMMENT),
+ comment=t.cast(dict, data.get(_GuiCoreContext.__PROP_ENTITY_COMMENT)),
)
entity.unlock_edit(self.gui._get_client_id())
_GuiCoreContext.__assign_var(state, error_var, "")
@@ -1184,7 +1191,7 @@ def on_dag_select(self, state: State, id: str, payload: t.Dict[str, str]):
try:
entity = (
core_get(args[0])
- if (reason := is_readable(args[0]))
+ if (reason := is_readable(t.cast(ScenarioId, args[0])))
else f"{args[0]} is not readable: {_get_reason(reason)}"
)
self.gui._call_function_with_state(
diff --git a/taipy/gui_core/filters.py b/taipy/gui_core/filters.py
new file mode 100644
index 0000000000..94870968d2
--- /dev/null
+++ b/taipy/gui_core/filters.py
@@ -0,0 +1,99 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import typing as t
+from dataclasses import dataclass
+from datetime import date, datetime
+
+from taipy.core import Scenario
+from taipy.gui.gui import _DoNotUpdate
+
+
+@dataclass
+class _Filter(_DoNotUpdate):
+ label: str
+ property_type: t.Optional[t.Type]
+
+ def get_property(self):
+ return self.label
+
+ def get_type(self):
+ if self.property_type is bool:
+ return "boolean"
+ elif self.property_type is int or self.property_type is float:
+ return "number"
+ elif self.property_type is datetime or self.property_type is date:
+ return "date"
+ elif self.property_type is str:
+ return "str"
+ return "any"
+
+
+@dataclass
+class ScenarioFilter(_Filter):
+ """
+ used to describe a filter on a scenario property
+ """
+
+ property_id: str
+
+ def get_property(self):
+ return self.property_id
+
+
+@dataclass
+class DataNodeScenarioFilter(_Filter):
+ """
+ used to describe a filter on a scenario datanode's property
+ """
+
+ datanode_config_id: str
+ property_id: str
+
+ def get_property(self):
+ return f"{self.datanode_config_id}.{self.property_id}"
+
+
+_CUSTOM_PREFIX = "fn:"
+
+
+@dataclass
+class CustomScenarioFilter(_Filter):
+ """
+ used to describe a custom scenario filter ie based on a user defined function
+ """
+
+ filter_function: t.Callable[[Scenario], t.Any]
+
+ def __post_init__(self):
+ if self.filter_function.__name__ == "":
+ raise TypeError("CustomScenarioFilter does not support lambda functions.")
+ mod = self.filter_function.__module__
+ self.module = mod if isinstance(mod, str) else mod.__name__
+
+ def get_property(self):
+ return f"{_CUSTOM_PREFIX}{self.module}:{self.filter_function.__name__}"
+
+ @staticmethod
+ def _get_custom(col: str) -> t.Optional[t.List[str]]:
+ return col[len(_CUSTOM_PREFIX) :].split(":") if col.startswith(_CUSTOM_PREFIX) else None
+
+
+@dataclass
+class DataNodeFilter(_Filter):
+ """
+ used to describe a filter on a datanode property
+ """
+
+ property_id: str
+
+ def get_property(self):
+ return self.property_id
diff --git a/tests/core/test_core.py b/tests/core/test_core.py
index 87a3f0e1eb..1932f05605 100644
--- a/tests/core/test_core.py
+++ b/tests/core/test_core.py
@@ -12,105 +12,20 @@
import pytest
-from taipy.config import Config
-from taipy.config.exceptions.exceptions import ConfigurationUpdateBlocked
-from taipy.core import Orchestrator
-from taipy.core._orchestrator._dispatcher import _DevelopmentJobDispatcher, _StandaloneJobDispatcher
-from taipy.core._orchestrator._orchestrator import _Orchestrator
-from taipy.core._orchestrator._orchestrator_factory import _OrchestratorFactory
-from taipy.core.config.job_config import JobConfig
-from taipy.core.exceptions.exceptions import OrchestratorServiceIsAlreadyRunning
+from taipy.core import Core, Orchestrator
-class TestOrchestrator:
- def test_run_orchestrator_trigger_config_check(self, caplog):
- Config.configure_data_node(id="d0", storage_type="toto")
- with pytest.raises(SystemExit):
- orchestrator = Orchestrator()
- orchestrator.run()
- expected_error_message = (
- "`storage_type` field of DataNodeConfig `d0` must be either csv, sql_table,"
- " sql, mongo_collection, pickle, excel, generic, json, parquet, s3_object, or in_memory."
- ' Current value of property `storage_type` is "toto".'
- )
- assert expected_error_message in caplog.text
- orchestrator.stop()
-
- def test_run_orchestrator_as_a_service_development_mode(self):
- _OrchestratorFactory._dispatcher = None
-
- orchestrator = Orchestrator()
- assert orchestrator._orchestrator is None
- assert orchestrator._dispatcher is None
- assert _OrchestratorFactory._dispatcher is None
-
- orchestrator.run()
- assert orchestrator._orchestrator is not None
- assert orchestrator._orchestrator == _Orchestrator
- assert _OrchestratorFactory._orchestrator is not None
- assert _OrchestratorFactory._orchestrator == _Orchestrator
- assert orchestrator._dispatcher is not None
- assert isinstance(orchestrator._dispatcher, _DevelopmentJobDispatcher)
- assert isinstance(_OrchestratorFactory._dispatcher, _DevelopmentJobDispatcher)
- orchestrator.stop()
-
- def test_run_orchestrator_as_a_service_standalone_mode(self):
- _OrchestratorFactory._dispatcher = None
-
- orchestrator = Orchestrator()
- assert orchestrator._orchestrator is None
-
- assert orchestrator._dispatcher is None
- assert _OrchestratorFactory._dispatcher is None
-
- Config.configure_job_executions(mode=JobConfig._STANDALONE_MODE, max_nb_of_workers=2)
- orchestrator.run()
- assert orchestrator._orchestrator is not None
- assert orchestrator._orchestrator == _Orchestrator
- assert _OrchestratorFactory._orchestrator is not None
- assert _OrchestratorFactory._orchestrator == _Orchestrator
- assert orchestrator._dispatcher is not None
- assert isinstance(orchestrator._dispatcher, _StandaloneJobDispatcher)
- assert isinstance(_OrchestratorFactory._dispatcher, _StandaloneJobDispatcher)
- assert orchestrator._dispatcher.is_running()
- assert _OrchestratorFactory._dispatcher.is_running()
- orchestrator.stop()
+class TestCore:
+ def test_run_core_with_depracated_message(self, caplog):
+ with pytest.warns(DeprecationWarning):
+ core = Core()
+ core.run()
- def test_orchestrator_service_can_only_be_run_once(self):
- orchestrator_instance_1 = Orchestrator()
- orchestrator_instance_2 = Orchestrator()
-
- orchestrator_instance_1.run()
-
- with pytest.raises(OrchestratorServiceIsAlreadyRunning):
- orchestrator_instance_1.run()
- with pytest.raises(OrchestratorServiceIsAlreadyRunning):
- orchestrator_instance_2.run()
-
- # Stop the Orchestrator service and run it again should work
- orchestrator_instance_1.stop()
-
- orchestrator_instance_1.run()
- orchestrator_instance_1.stop()
- orchestrator_instance_2.run()
- orchestrator_instance_2.stop()
-
- def test_block_config_update_when_orchestrator_service_is_running_development_mode(self):
- _OrchestratorFactory._dispatcher = None
-
- orchestrator = Orchestrator()
- orchestrator.run()
- with pytest.raises(ConfigurationUpdateBlocked):
- Config.configure_data_node(id="i1")
- orchestrator.stop()
-
- @pytest.mark.standalone
- def test_block_config_update_when_orchestrator_service_is_running_standalone_mode(self):
- _OrchestratorFactory._dispatcher = None
+ assert isinstance(core, Orchestrator)
+ expected_message = (
+ "The `Core` service is deprecated and replaced by the `Orchestrator` service. "
+ "An `Orchestrator` instance has been instantiated instead."
+ )
+ assert expected_message in caplog.text
- orchestrator = Orchestrator()
- Config.configure_job_executions(mode=JobConfig._STANDALONE_MODE, max_nb_of_workers=2)
- orchestrator.run()
- with pytest.raises(ConfigurationUpdateBlocked):
- Config.configure_data_node(id="i1")
- orchestrator.stop()
+ core.stop()
diff --git a/tests/core/test_orchestrator.py b/tests/core/test_orchestrator.py
new file mode 100644
index 0000000000..87a3f0e1eb
--- /dev/null
+++ b/tests/core/test_orchestrator.py
@@ -0,0 +1,116 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+
+import pytest
+
+from taipy.config import Config
+from taipy.config.exceptions.exceptions import ConfigurationUpdateBlocked
+from taipy.core import Orchestrator
+from taipy.core._orchestrator._dispatcher import _DevelopmentJobDispatcher, _StandaloneJobDispatcher
+from taipy.core._orchestrator._orchestrator import _Orchestrator
+from taipy.core._orchestrator._orchestrator_factory import _OrchestratorFactory
+from taipy.core.config.job_config import JobConfig
+from taipy.core.exceptions.exceptions import OrchestratorServiceIsAlreadyRunning
+
+
+class TestOrchestrator:
+ def test_run_orchestrator_trigger_config_check(self, caplog):
+ Config.configure_data_node(id="d0", storage_type="toto")
+ with pytest.raises(SystemExit):
+ orchestrator = Orchestrator()
+ orchestrator.run()
+ expected_error_message = (
+ "`storage_type` field of DataNodeConfig `d0` must be either csv, sql_table,"
+ " sql, mongo_collection, pickle, excel, generic, json, parquet, s3_object, or in_memory."
+ ' Current value of property `storage_type` is "toto".'
+ )
+ assert expected_error_message in caplog.text
+ orchestrator.stop()
+
+ def test_run_orchestrator_as_a_service_development_mode(self):
+ _OrchestratorFactory._dispatcher = None
+
+ orchestrator = Orchestrator()
+ assert orchestrator._orchestrator is None
+ assert orchestrator._dispatcher is None
+ assert _OrchestratorFactory._dispatcher is None
+
+ orchestrator.run()
+ assert orchestrator._orchestrator is not None
+ assert orchestrator._orchestrator == _Orchestrator
+ assert _OrchestratorFactory._orchestrator is not None
+ assert _OrchestratorFactory._orchestrator == _Orchestrator
+ assert orchestrator._dispatcher is not None
+ assert isinstance(orchestrator._dispatcher, _DevelopmentJobDispatcher)
+ assert isinstance(_OrchestratorFactory._dispatcher, _DevelopmentJobDispatcher)
+ orchestrator.stop()
+
+ def test_run_orchestrator_as_a_service_standalone_mode(self):
+ _OrchestratorFactory._dispatcher = None
+
+ orchestrator = Orchestrator()
+ assert orchestrator._orchestrator is None
+
+ assert orchestrator._dispatcher is None
+ assert _OrchestratorFactory._dispatcher is None
+
+ Config.configure_job_executions(mode=JobConfig._STANDALONE_MODE, max_nb_of_workers=2)
+ orchestrator.run()
+ assert orchestrator._orchestrator is not None
+ assert orchestrator._orchestrator == _Orchestrator
+ assert _OrchestratorFactory._orchestrator is not None
+ assert _OrchestratorFactory._orchestrator == _Orchestrator
+ assert orchestrator._dispatcher is not None
+ assert isinstance(orchestrator._dispatcher, _StandaloneJobDispatcher)
+ assert isinstance(_OrchestratorFactory._dispatcher, _StandaloneJobDispatcher)
+ assert orchestrator._dispatcher.is_running()
+ assert _OrchestratorFactory._dispatcher.is_running()
+ orchestrator.stop()
+
+ def test_orchestrator_service_can_only_be_run_once(self):
+ orchestrator_instance_1 = Orchestrator()
+ orchestrator_instance_2 = Orchestrator()
+
+ orchestrator_instance_1.run()
+
+ with pytest.raises(OrchestratorServiceIsAlreadyRunning):
+ orchestrator_instance_1.run()
+ with pytest.raises(OrchestratorServiceIsAlreadyRunning):
+ orchestrator_instance_2.run()
+
+ # Stop the Orchestrator service and run it again should work
+ orchestrator_instance_1.stop()
+
+ orchestrator_instance_1.run()
+ orchestrator_instance_1.stop()
+ orchestrator_instance_2.run()
+ orchestrator_instance_2.stop()
+
+ def test_block_config_update_when_orchestrator_service_is_running_development_mode(self):
+ _OrchestratorFactory._dispatcher = None
+
+ orchestrator = Orchestrator()
+ orchestrator.run()
+ with pytest.raises(ConfigurationUpdateBlocked):
+ Config.configure_data_node(id="i1")
+ orchestrator.stop()
+
+ @pytest.mark.standalone
+ def test_block_config_update_when_orchestrator_service_is_running_standalone_mode(self):
+ _OrchestratorFactory._dispatcher = None
+
+ orchestrator = Orchestrator()
+ Config.configure_job_executions(mode=JobConfig._STANDALONE_MODE, max_nb_of_workers=2)
+ orchestrator.run()
+ with pytest.raises(ConfigurationUpdateBlocked):
+ Config.configure_data_node(id="i1")
+ orchestrator.stop()
diff --git a/tests/gui_core/test_context_is_readable.py b/tests/gui_core/test_context_is_readable.py
index cd9b389cfe..2514bc18c6 100644
--- a/tests/gui_core/test_context_is_readable.py
+++ b/tests/gui_core/test_context_is_readable.py
@@ -178,7 +178,7 @@ def test_submission_status_callback(self):
with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get) as mockget:
mockget.reset_mock()
mockGui = Mock(Gui)
- mockGui._get_autorization = lambda s: contextlib.nullcontext()
+ mockGui._get_authorization = lambda s: contextlib.nullcontext()
gui_core_context = _GuiCoreContext(mockGui)
def sub_cb():