/src/iop $IRISINSTALLDIR/python/iop
43 | ```
44 |
45 | ##### Run the unit tests.
46 |
47 | ```bash
48 | pytest
49 | ```
50 |
51 | #### Docker installation of IRIS
52 |
53 | No prerequisites are needed. Just run the following command:
54 |
55 | ```bash
56 | docker build -t pytest-iris -f dockerfile-ci .
57 | docker run -i --rm pytest-iris
58 | ```
59 |
--------------------------------------------------------------------------------
/docs/credits.md:
--------------------------------------------------------------------------------
1 | # Credits
2 |
3 | Most of the code came from PEX for Python by Mo Cheng and Summer Gerry.
4 |
--------------------------------------------------------------------------------
/docs/getting-started/first-steps.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Interoperability On Python
2 |
3 | Welcome to the guide on getting started with Interoperability Embedded Python. This document will walk you through the initial steps to set up and begin using Python in your interoperability projects.
4 |
5 | ## Prerequisites
6 |
7 | Before you begin, ensure you have the following:
8 |
9 | - A working installation of InterSystems IRIS with Embedded Python configured
10 | - Basic knowledge of Python programming
11 |
12 | ### Setting Up the Virtual Environment
13 |
14 | To begin, you will need to set up a virtual environment for your Python project. A virtual environment is a self-contained directory that contains a Python installation for a particular version of Python, as well as any additional packages you may need for your project.
15 |
16 | To create a virtual environment, run the following command in your terminal:
17 |
18 | ```bash
19 | python -m venv .venv
20 | ```
21 |
22 | This will create a new directory called `.venv` in your project directory, which will contain the Python interpreter and any packages you install.
23 |
24 | Next, activate the virtual environment by running the following command:
25 |
26 | For Unix or MacOS:
27 |
28 | ```bash
29 | source .venv/bin/activate
30 | ```
31 |
32 | For Windows:
33 |
34 | ```bash
35 | .venv\Scripts\activate
36 | ```
37 |
38 | You should now see the name of your virtual environment in your terminal prompt, indicating that the virtual environment is active.
39 |
40 | ### Installing Required Packages
41 |
42 | With your virtual environment activated, you can now install any required packages for your project. To install a package, use the `pip` command followed by the package name. For example, to install the `iris-pex-embedded-python` package, run the following command:
43 |
44 | ```bash
45 | pip install iris-pex-embedded-python
46 | ```
47 |
48 | Init the application using the following command:
49 |
50 | ```bash
51 | iop --init
52 | ```
53 |
54 | This will install the package and any dependencies it requires.
55 |
56 | ## Hello World
57 |
58 | Now that you have set up your virtual environment and installed the required packages, you are ready to create your first Interoperability production using Python.
59 |
60 | ### Create a Business Operation
61 |
62 | For this, we will create an `BusinessOperation` that will take a message as input and will return a message as output. In between, it will just print "Hello World" in the logs.
63 |
64 | To do this, let's create a new folder named `hello_world`.
65 |
66 | ```bash
67 | mkdir hello_world
68 | ```
69 |
70 | In this folder, create a new file named `bo.py`.
71 |
72 | This file will contain the code of our business operation.
73 |
74 | ```python
75 | from iop import BusinessOperation
76 |
77 | class MyBo(BusinessOperation):
78 | def on_message(self, request):
79 | self.log_info("Hello World")
80 | ```
81 |
82 | Let's explain this code.
83 |
84 | First, we import the `BusinessOperation` class from the `iop` module.
85 |
86 | Then, we create a class named `MyBo` that inherits from `BusinessOperation`.
87 |
88 | Finally, we override the `on_message` method. This method will be called when a message is received by the business operation.
89 |
90 | ### Import this Business Operation in the framework
91 |
92 | Now, we need to add this business operation to what we call a production.
93 |
94 | To do this, we will create a new file in the `hello_world` folder, named `settings.py`.
95 |
96 | Every project starts at it's root folder by a file named `settings.py`.
97 |
98 | This file contains two main settings:
99 |
100 | - `CLASSES` : it contains the classes that will be used in the project.
101 | - `PRODUCTIONS` : it contains the name of the production that will be used in the project.
102 |
103 | ```python
104 | from hello_world.bo import MyBo
105 |
106 | CLASSES = {
107 | "MyIRIS.MyBo": MyBo
108 | }
109 |
110 | PRODUCTIONS = [
111 | {
112 | 'MyIRIS.Production': {
113 | "@TestingEnabled": "true",
114 | "Item": [
115 | {
116 | "@Name": "Instance.Of.MyBo",
117 | "@ClassName": "MyIRIS.MyBo",
118 | }
119 | ]
120 | }
121 | }
122 | ]
123 | ```
124 |
125 | In this file, we import our `MyBo` class named in iris `MyIRIS.MyBo`, and we add it to the `CLASSES` dictionnary.
126 |
127 | Then, we add a new production to the `PRODUCTIONS` list. This production will contain our `MyBo` class instance named `Instance.Of.MyBo`.
128 |
129 | With the `iop` command, we can now create the production in IRIS.
130 |
131 | ```bash
132 | iop --migrate /path/to/hello_world/settings.py
133 | ```
134 |
135 | This command will create the production in IRIS and add the `MyBo` class to it.
136 |
137 | More information about registering components can be found [here](register-component.md).
138 |
--------------------------------------------------------------------------------
/docs/getting-started/installation.md:
--------------------------------------------------------------------------------
1 | # Installation Guide
2 |
3 | Welcome to the installation guide for IoP. This guide will walk you through the steps to install the application on your local machine or in a docker container image.
4 |
5 | ## Prerequisites
6 |
7 | Before you begin, ensure you have the following installed:
8 |
9 | - Python 3.6 or higher
10 | - IRIS 2021.2 or higher
11 | - [Configuring Embedded Python](https://grongierisc.github.io/iris-embedded-python-wrapper/)
12 |
13 | ## With PyPi
14 |
15 | To install the application using PyPi, run the following command:
16 |
17 | ```bash
18 | pip install iris-pex-embedded-python
19 | ```
20 |
21 | Then you can run the application using the following command:
22 |
23 | ```bash
24 | iop --init
25 | ```
26 |
27 | Check the documentation about the command line interface [here](/interoperability-embedded-python/command-line) for more information.
28 |
29 | ## With ZPM/IPM
30 |
31 | To install the application using ZPM or IPM, run the following command:
32 |
33 | ```objectscript
34 | zpm "install iris-pex-embedded-python"
35 | ```
36 |
37 |
--------------------------------------------------------------------------------
/docs/img/IoPRemoteDebug.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/IoPRemoteDebug.mp4
--------------------------------------------------------------------------------
/docs/img/RemoteDebugOptions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/RemoteDebugOptions.png
--------------------------------------------------------------------------------
/docs/img/RemoteDebugSuccess.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/RemoteDebugSuccess.png
--------------------------------------------------------------------------------
/docs/img/RemoteDebugToLate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/RemoteDebugToLate.png
--------------------------------------------------------------------------------
/docs/img/RemoteDebugWaiting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/RemoteDebugWaiting.png
--------------------------------------------------------------------------------
/docs/img/complex_transform.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/complex_transform.png
--------------------------------------------------------------------------------
/docs/img/custom_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/custom_settings.png
--------------------------------------------------------------------------------
/docs/img/dtl_wizard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/dtl_wizard.png
--------------------------------------------------------------------------------
/docs/img/interop_dtl_management_portal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/interop_dtl_management_portal.png
--------------------------------------------------------------------------------
/docs/img/settings-in-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/settings-in-production.png
--------------------------------------------------------------------------------
/docs/img/test_dtl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/test_dtl.png
--------------------------------------------------------------------------------
/docs/img/traceback_disable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/traceback_disable.png
--------------------------------------------------------------------------------
/docs/img/traceback_enable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/traceback_enable.png
--------------------------------------------------------------------------------
/docs/img/vdoc_type.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/vdoc_type.png
--------------------------------------------------------------------------------
/docs/img/vscode_breakpoint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/vscode_breakpoint.png
--------------------------------------------------------------------------------
/docs/img/vscode_debug.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/vscode_debug.png
--------------------------------------------------------------------------------
/docs/img/vscode_debugging.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/vscode_debugging.png
--------------------------------------------------------------------------------
/docs/img/vscode_open.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/vscode_open.png
--------------------------------------------------------------------------------
/docs/img/vscode_python_extension.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/vscode_python_extension.png
--------------------------------------------------------------------------------
/docs/img/vscode_select_venv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/docs/img/vscode_select_venv.png
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # IoP (Interoperability On Python)
2 |
3 | [](https://pypi.org/project/iris-pex-embedded-python/)
4 | [](https://pypi.org/project/iris-pex-embedded-python/)
5 | [](https://pypi.org/project/iris-pex-embedded-python/)
6 | [](https://pypi.org/project/iris-pex-embedded-python/)
7 | 
8 |
9 | Welcome to the **Interoperability On Python (IoP)** proof of concept! This project demonstrates how the **IRIS Interoperability Framework** can be utilized with a **Python-first approach**.
10 |
11 | ## Example
12 |
13 | Here's a simple example of how a Business Operation can be implemented in Python:
14 |
15 | ```python
16 | from iop import BusinessOperation
17 |
18 | class MyBo(BusinessOperation):
19 | def on_message(self, request):
20 | self.log_info("Hello World")
21 | ```
22 |
23 | ## Installation
24 |
25 | To start using this proof of concept, install it using pip:
26 |
27 | ```bash
28 | pip install iris-pex-embedded-python
29 | ```
30 |
31 | ## Getting Started
32 |
33 | If you're new to this proof of concept, begin by reading the [installation guide](getting-started/installation). Then, follow the [first steps](getting-started/first-steps) to create your first Business Operation.
34 |
35 | Happy coding!
--------------------------------------------------------------------------------
/docs/logging.md:
--------------------------------------------------------------------------------
1 | # Logging
2 |
3 | InterSystems IRIS Interoperability framework implements its own logging system. The Python API provides a way to use Python's logging module integrated with IRIS logging.
4 |
5 | ## Basic Usage
6 |
7 | The logging system is available through the component base class. You can access it via the `logger` property or use the convenience methods:
8 |
9 | ```python
10 | def on_init(self):
11 | # Using convenience methods
12 | self.log_info("Component initialized")
13 | self.log_error("An error occurred")
14 | self.log_warning("Warning message")
15 | self.log_alert("Critical alert")
16 | self.trace("Debug trace message")
17 |
18 | # Using logger property
19 | self.logger.info("Info via logger")
20 | self.logger.error("Error via logger")
21 | ```
22 |
23 | ## Console Logging
24 |
25 | You can direct logs to the console instead of IRIS in two ways:
26 |
27 | 1. Set the component-wide setting:
28 | ```python
29 | def on_init(self):
30 | self.log_to_console = True
31 | self.log_info("This will go to console")
32 | ```
33 |
34 | 2. Per-message console logging:
35 | ```python
36 | def on_message(self, request):
37 | # Log specific message to console
38 | self.log_info("Debug info", to_console=True)
39 |
40 | # Other logs still go to IRIS
41 | self.log_info("Production info")
42 | ```
43 |
44 | ## Log Levels
45 |
46 | The following log levels are available:
47 |
48 | - `trace()` - Debug level logging (maps to IRIS LogTrace)
49 | - `log_info()` - Information messages (maps to IRIS LogInfo)
50 | - `log_warning()` - Warning messages (maps to IRIS LogWarning)
51 | - `log_error()` - Error messages (maps to IRIS LogError)
52 | - `log_alert()` - Critical/Alert messages (maps to IRIS LogAlert)
53 | - `log_assert()` - Assert messages (maps to IRIS LogAssert)
54 |
55 | ## Integration with IRIS
56 |
57 | The Python logging is automatically mapped to the appropriate IRIS logging methods:
58 |
59 | - Python `DEBUG` → IRIS `LogTrace`
60 | - Python `INFO` → IRIS `LogInfo`
61 | - Python `WARNING` → IRIS `LogWarning`
62 | - Python `ERROR` → IRIS `LogError`
63 | - Python `CRITICAL` → IRIS `LogAlert`
64 |
65 | ## Legacy Methods
66 |
67 | The following methods are deprecated but maintained for backwards compatibility:
68 |
69 | - `LOGINFO()` - Use `log_info()` instead
70 | - `LOGALERT()` - Use `log_alert()` instead
71 | - `LOGWARNING()` - Use `log_warning()` instead
72 | - `LOGERROR()` - Use `log_error()` instead
73 | - `LOGASSERT()` - Use `log_assert()` instead
74 |
--------------------------------------------------------------------------------
/docs/prod-settings.md:
--------------------------------------------------------------------------------
1 | # Settings in production
2 |
3 | To pass production settings to your component, you have two options:
4 |
5 | - Use the **%settings** parameter
6 | - Create your custom settings
7 |
8 | ## Context
9 |
10 | In production when you select a component, you can configure it by passing settings.
11 |
12 | 
13 |
14 | Those settings can be passed to your python code.
15 |
16 | ## Use the %settings parameter
17 |
18 | All the settings passed to **%settings** are available in string format into your class as a root attribute.
19 |
20 | Each line of the **%settings** parameter is a key-value pair separated by a the equal sign.
21 |
22 | Key will be the name of the attribute and value will be the value of the attribute.
23 |
24 | For example, if you have the following settings:
25 |
26 | ```text
27 | foo=bar
28 | my_number=42
29 | ```
30 |
31 | You can access those settings in your class like this:
32 |
33 | ```python
34 | from iop import BusinessOperation
35 |
36 | class MyBusinessOperation(BusinessOperation):
37 |
38 | def on_init(self):
39 | self.log_info("[Python] MyBusinessOperation:on_init() is called")
40 | self.log_info("[Python] foo: " + self.foo)
41 | self.log_info("[Python] my_number: " + self.my_number)
42 | return
43 | ```
44 |
45 | As **%settings** is a free text field, you can pass any settings you want.
46 |
47 | Meaning you should verify if the attribute exists before using it.
48 |
49 | ```python
50 | from iop import BusinessOperation
51 |
52 | class MyBusinessOperation(BusinessOperation):
53 |
54 | def on_init(self):
55 | self.log_info("[Python] MyBusinessOperation:on_init() is called")
56 | if hasattr(self, 'foo'):
57 | self.log_info("[Python] foo: " + self.foo)
58 | if hasattr(self, 'my_number'):
59 | self.log_info("[Python] my_number: " + self.my_number)
60 | return
61 | ```
62 |
63 | ## Create your custom settings
64 |
65 | If you want to have a more structured way to pass settings, you can create your custom settings.
66 |
67 | To create a custom settings, you create an attribute in your class.
68 |
69 | This attribute must :
70 |
71 | - have an default value.
72 | - don't start with an underscore.
73 | - be untyped or have the following types: `str`, `int`, `float`, `bool`.
74 |
75 | Otherwise, it will not be available in the managment portal.
76 |
77 | ```python
78 | from iop import BusinessOperation
79 |
80 | class MyBusinessOperation(BusinessOperation):
81 |
82 | # This setting will be available in the managment portal
83 | foo: str = "default"
84 | my_number: int = 42
85 | untyped_setting = None
86 |
87 | # This setting will not be available in the managment portal
88 | _my_internal_setting: str = "default"
89 | no_aviable_setting
90 |
91 | def on_init(self):
92 | self.log_info("[Python] MyBusinessOperation:on_init() is called")
93 | self.log_info("[Python] foo: " + self.foo)
94 | self.log_info("[Python] my_number: " + str(self.my_number))
95 | return
96 | ```
97 |
98 | They will be available in the managment portal as the following:
99 |
100 | 
101 |
102 | If you overwrite the default value in the managment portal, the new value will be passed to your class.
--------------------------------------------------------------------------------
/docs/useful-links.md:
--------------------------------------------------------------------------------
1 | ## Useful Links
2 |
3 | Explore the following resources to learn more about the project and see it in action:
4 |
5 | ### Training
6 | - [Training Repository](https://github.com/grongierisc/formation-template-python)
7 | A repository to learn how to use the project.
8 |
9 | ### Templates
10 | - [Project Template](https://github.com/grongierisc/iris-python-interoperability-template)
11 | A template to start a new project.
12 |
13 | ### Project Links
14 | - [PyPI](https://pypi.org/project/iris-pex-embedded-python/)
15 | The project on PyPI.
16 | - [GitHub](https://github.com/grongierisc/interoperability-embedded-python)
17 | The project on GitHub.
18 |
19 | ### Demos
20 | - [Flask Demo](https://github.com/grongierisc/iris-flask-template)
21 | Demonstrates how to use the project with Flask.
22 | - [FastAPI Demo](https://github.com/grongierisc/iris-fastapi-template)
23 | Demonstrates how to use the project with FastAPI.
24 | - [Django Demo](https://github.com/grongierisc/iris-django-template)
25 | Demonstrates how to use the project with Django.
26 | - [Kafka Demo](https://github.com/grongierisc/iris-kafka-python)
27 | Example of using the project with Kafka.
28 | - [RAG Demo](https://github.com/grongierisc/iris-rag-demo)
29 | Example of using the project with RAG.
30 | - [IRIS Chemical](https://github.com/grongierisc/iris-chemicals-properties)
31 | Example of extracting chemical properties.
32 | - [Rest to DICOM](https://github.com/grongierisc/RestToDicom)
33 | Example of using the project with RestToDicom.
34 | - [OCR](https://github.com/grongierisc/iris-pero-ocr)
35 | Example of using the project with an OCR engine.
36 |
--------------------------------------------------------------------------------
/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # start iris
4 | /iris-main "$@" &
5 |
6 | /usr/irissys/dev/Cloud/ICM/waitISC.sh
7 |
8 | alias iop='irispython -m iop'
9 |
10 | # init iop
11 | iop --init
12 |
13 | # load production
14 | iop -m /irisdev/app/demo/python/reddit/settings.py
15 |
16 | # set default production
17 | iop --default PEX.Production
18 |
19 | # start production
20 | iop --start
21 |
22 |
--------------------------------------------------------------------------------
/key/.gitignore:
--------------------------------------------------------------------------------
1 | *.key
--------------------------------------------------------------------------------
/merge.cpf:
--------------------------------------------------------------------------------
1 | [Actions]
2 | CreateResource:Name=%DB_IRISAPP_DATA,Description="IRISAPP_DATA database"
3 | CreateDatabase:Name=IRISAPP_DATA,Directory=/usr/irissys/mgr/IRISAPP_DATA,Resource=%DB_IRISAPP_DATA
4 | CreateResource:Name=%DB_IRISAPP_CODE,Description="IRISAPP_CODE database"
5 | CreateDatabase:Name=IRISAPP_CODE,Directory=/usr/irissys/mgr/IRISAPP_CODE,Resource=%DB_IRISAPP_CODE
6 | CreateNamespace:Name=IRISAPP,Globals=IRISAPP_DATA,Routines=IRISAPP_CODE,Interop=1
7 | ModifyService:Name=%Service_CallIn,Enabled=1,AutheEnabled=48
8 | ModifyUser:Name=SuperUser,ChangePassword=0,PasswordHash=a31d24aecc0bfe560a7e45bd913ad27c667dc25a75cbfd358c451bb595b6bd52bd25c82cafaa23ca1dd30b3b4947d12d3bb0ffb2a717df29912b743a281f97c1,0a4c463a2fa1e7542b61aa48800091ab688eb0a14bebf536638f411f5454c9343b9aa6402b4694f0a89b624407a5f43f0a38fc35216bb18aab7dc41ef9f056b1,10000,SHA512
9 |
--------------------------------------------------------------------------------
/misc/component-config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/misc/component-config.png
--------------------------------------------------------------------------------
/misc/interop-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/misc/interop-screenshot.png
--------------------------------------------------------------------------------
/misc/json-message-trace.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/misc/json-message-trace.png
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: IoP (Interoperability On Python)
2 | site_description: A Python-first approach to build Interoperability solutions on IRIS and Health Connect
3 | site_author: grongier@intersystems.com
4 | site_url: https://grongierisc.github.io/interoperability-embedded-python/
5 | docs_dir: docs
6 | nav:
7 | - Home: index.md
8 | - Getting Started:
9 | - Installation: getting-started/installation.md
10 | - First Steps: getting-started/first-steps.md
11 | - Register Component: getting-started/register-component.md
12 | - API documentation:
13 | - Command Line Interface: command-line.md
14 | - Python API: python-api.md
15 | - DTL Support: dtl.md
16 | - Logging: logging.md
17 | - Debugging: debug.md
18 | - Production Settings: prod-settings.md
19 | - Contributing:
20 | - Contributing: contributing.md
21 | - Code of Conduct: code-of-conduct.md
22 | - Reference:
23 | - Examples: example.md
24 | - Useful Links: useful-links.md
25 | - About:
26 | - Changelog: changelog.md
27 | - Credits: credits.md
28 | - Benchmarks: benchmarks.md
29 |
30 | theme: readthedocs
31 |
32 | markdown_extensions:
33 | - pymdownx.snippets
--------------------------------------------------------------------------------
/module.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | pex-embbeded-python
6 | 3.4.0
7 | Hack of PEX Python but for Embedded Python
8 | python
9 |
10 | module
11 | src/iop/cls
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/output/.gitignore:
--------------------------------------------------------------------------------
1 | Cat.txt
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools", "wheel"]
3 |
4 | [project]
5 | name = "iris_pex_embedded_python"
6 | version = "3.4.4"
7 | description = "Iris Interoperability based on Embedded Python"
8 | readme = "README.md"
9 | authors = [
10 | { name = "grongier", email = "guillaume.rongier@intersystems.com" },
11 | ]
12 | keywords = ["iris", "intersystems", "python", "embedded"]
13 |
14 | classifiers = [
15 | "Development Status :: 5 - Production/Stable",
16 | "Intended Audience :: Developers",
17 | "License :: OSI Approved :: MIT License",
18 | "Operating System :: OS Independent",
19 | "Programming Language :: Python :: 3.6",
20 | "Programming Language :: Python :: 3.7",
21 | "Programming Language :: Python :: 3.8",
22 | "Programming Language :: Python :: 3.9",
23 | "Programming Language :: Python :: 3.10",
24 | "Programming Language :: Python :: 3.11",
25 | "Programming Language :: Python :: 3.12",
26 | "Topic :: Utilities"
27 | ]
28 |
29 | dependencies = [
30 | "pydantic>=2.0.0",
31 | "xmltodict>=0.12.0",
32 | "iris-embedded-python-wrapper>=0.0.6",
33 | "jsonpath-ng>=1.7.0",
34 | "debugpy>=1.8.0",
35 | ]
36 |
37 | license = { file = "LICENSE" }
38 |
39 | [project.urls]
40 | homepage = "https://github.com/grongierisc/interoperability-embedded-python"
41 | documentation = "https://github.com/grongierisc/interoperability-embedded-python/blob/master/README.md"
42 | repository = "https://github.com/grongierisc/interoperability-embedded-python"
43 | issues = "https://github.com/grongierisc/interoperability-embedded-python/issues"
44 |
45 | [project.scripts]
46 | iop = "iop._cli:main"
47 |
48 | [tool.setuptools.packages.find]
49 | where = ["src"]
50 | exclude = ["tests*"]
51 |
52 | [tool.setuptools.package-data]
53 | "*" = ["*.cls"]
54 |
55 | [tool.pytest.ini_options]
56 | asyncio_default_fixture_loop_scope = "class"
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | pytest
2 | pytest-asyncio
3 | xmltodict
4 | requests
5 | dataclasses-json
6 | wheel
7 | twine
8 | iris-embedded-python-wrapper
9 | jsonpath-ng
10 | pydantic>=2.0.0
11 | mkdocs
12 | pymdown-extensions
13 | debugpy
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | iris-pex-embedded-python>=3.0.0
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup()
--------------------------------------------------------------------------------
/src/grongier/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grongierisc/interoperability-embedded-python/b478c3985d51ca70cb3d1334ce05730e4722d677/src/grongier/__init__.py
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/BusinessOperation.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2021 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Class Grongier.PEX.BusinessOperation Extends IOP.BusinessOperation [ Inheritance = right, ProcedureBlock, System = 4 ]
6 | {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/BusinessProcess.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2021 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Class Grongier.PEX.BusinessProcess Extends IOP.BusinessProcess [ Inheritance = right, ProcedureBlock, System = 4 ]
6 | {
7 |
8 | Storage Default
9 | {
10 | %Storage.Persistent
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/BusinessService.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2021 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Class Grongier.PEX.BusinessService Extends IOP.BusinessService [ Inheritance = right, ProcedureBlock, System = 4 ]
6 | {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/Common.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2021 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Include Ensemble
6 |
7 | Class Grongier.PEX.Common Extends IOP.Common [ Abstract, ClassType = "", ProcedureBlock, System = 4 ]
8 | {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/Director.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2021 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Include (%occInclude, Ensemble)
6 |
7 | Class Grongier.PEX.Director Extends IOP.Director [ Inheritance = right, ProcedureBlock, System = 4 ]
8 | {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/Duplex/Operation.cls:
--------------------------------------------------------------------------------
1 | Class Grongier.PEX.DuplexOperation Extends IOP.DuplexOperation
2 | {
3 |
4 | }
5 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/Duplex/Process.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2022 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Class Grongier.PEX.DuplexProcess Extends IOP.BusinessProcess [ ClassType = persistent, ProcedureBlock, System = 4 ]
6 | {
7 |
8 | Storage Default
9 | {
10 | %Storage.Persistent
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/Duplex/Service.cls:
--------------------------------------------------------------------------------
1 | Class Grongier.PEX.DuplexService Extends IOP.PrivateSessionDuplex
2 | {
3 |
4 | }
5 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/InboundAdapter.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2021 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Class Grongier.PEX.InboundAdapter Extends IOP.InboundAdapter [ Inheritance = right, ProcedureBlock, System = 4 ]
6 | {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/Message.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2021 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Class Grongier.PEX.Message Extends IOP.Message
6 | {
7 |
8 | Storage Default
9 | {
10 | %Storage.Persistent
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/OutboundAdapter.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2021 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Class Grongier.PEX.OutboundAdapter Extends IOP.OutboundAdapter [ Inheritance = right, ProcedureBlock, System = 4 ]
6 | {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/PickleMessage.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2021 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Class Grongier.PEX.PickleMessage Extends IOP.PickleMessage
6 | {
7 |
8 | Storage Default
9 | {
10 | %Storage.Persistent
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2022 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Class Grongier.PEX.PrivateSessionDuplex Extends IOP.PrivateSessionDuplex [ Abstract, System = 4 ]
6 | {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2022 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | /// This class is a DICOM framework class
6 | Class Grongier.PEX.PrivateSession.Message.Ack Extends IOP.PrivateSession.Message.Ack [ ClassType = persistent, Inheritance = right, ProcedureBlock, System = 4 ]
7 | {
8 |
9 | Storage Default
10 | {
11 | %Storage.Persistent
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2022 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | /// This class is a DICOM framework class
6 | Class Grongier.PEX.PrivateSession.Message.Poll Extends IOP.PrivateSession.Message.Poll [ ClassType = persistent, Inheritance = right, ProcedureBlock, System = 4 ]
7 | {
8 |
9 | Storage Default
10 | {
11 | %Storage.Persistent
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2022 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | /// This class is a DICOM framework class
6 | Class Grongier.PEX.PrivateSession.Message.Start Extends IOP.PrivateSession.Message.Start [ ClassType = persistent, Inheritance = right, ProcedureBlock, System = 4 ]
7 | {
8 |
9 | Storage Default
10 | {
11 | %Storage.Persistent
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2022 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | /// This class is a DICOM framework class
6 | Class Grongier.PEX.PrivateSession.Message.Stop Extends IOP.PrivateSession.Message.Stop [ ClassType = persistent, Inheritance = right, ProcedureBlock, System = 4 ]
7 | {
8 |
9 | Storage Default
10 | {
11 | %Storage.Persistent
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/Test.cls:
--------------------------------------------------------------------------------
1 | /// Description
2 | Class Grongier.PEX.Test Extends IOP.Test
3 | {
4 |
5 | Storage Default
6 | {
7 | %Storage.Persistent
8 | }
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/PEX/Utils.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2021 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Include Ensemble
6 |
7 | Class Grongier.PEX.Utils Extends IOP.Utils
8 | {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/src/grongier/cls/Grongier/Service/WSGI.cls:
--------------------------------------------------------------------------------
1 | Class Grongier.Service.WSGI Extends IOP.Service.WSGI [ ServerOnly = 1 ]
2 | {
3 |
4 | }
5 |
--------------------------------------------------------------------------------
/src/grongier/pex/__init__.py:
--------------------------------------------------------------------------------
1 | from iop._business_service import _BusinessService
2 | from iop._business_process import _BusinessProcess
3 | from iop._private_session_duplex import _PrivateSessionDuplex
4 | from iop._private_session_process import _PrivateSessionProcess
5 | from iop._business_operation import _BusinessOperation
6 | from iop._inbound_adapter import _InboundAdapter
7 | from iop._outbound_adapter import _OutboundAdapter
8 | from iop._message import _Message
9 | from iop._message import _PickleMessage
10 | from iop._director import _Director
11 | from iop._utils import _Utils
12 |
13 | class Utils(_Utils): pass
14 | class InboundAdapter(_InboundAdapter): pass
15 | class OutboundAdapter(_OutboundAdapter): pass
16 | class BusinessService(_BusinessService): pass
17 | class BusinessOperation(_BusinessOperation): pass
18 | class BusinessProcess(_BusinessProcess): pass
19 | class DuplexService(_PrivateSessionDuplex): pass
20 | class DuplexOperation(_PrivateSessionDuplex): pass
21 | class DuplexProcess(_PrivateSessionProcess): pass
22 | class Message(_Message): pass
23 | class PickleMessage(_PickleMessage): pass
24 | class Director(_Director): pass
25 |
--------------------------------------------------------------------------------
/src/grongier/pex/__main__.py:
--------------------------------------------------------------------------------
1 | # main entry is _cli.main()
2 | if __name__ == '__main__':
3 | import iop._cli as _cli
4 | _cli.main()
--------------------------------------------------------------------------------
/src/grongier/pex/_business_host.py:
--------------------------------------------------------------------------------
1 | from iop._business_host import _BusinessHost
--------------------------------------------------------------------------------
/src/grongier/pex/_cli.py:
--------------------------------------------------------------------------------
1 | from iop._cli import main
2 |
3 | if __name__ == '__main__':
4 | main()
--------------------------------------------------------------------------------
/src/grongier/pex/_common.py:
--------------------------------------------------------------------------------
1 | from iop._common import _Common
--------------------------------------------------------------------------------
/src/grongier/pex/_director.py:
--------------------------------------------------------------------------------
1 | from iop._director import _Director
--------------------------------------------------------------------------------
/src/grongier/pex/_utils.py:
--------------------------------------------------------------------------------
1 | from iop._utils import _Utils
--------------------------------------------------------------------------------
/src/grongier/pex/wsgi/handlers.py:
--------------------------------------------------------------------------------
1 | import os, sys, importlib, urllib.parse
2 | from io import BytesIO
3 |
4 | __ospath = os.getcwd()
5 |
6 | import iris
7 |
8 | os.chdir(__ospath)
9 |
10 | enc, esc = sys.getfilesystemencoding(), 'surrogateescape'
11 |
12 | rest_service = iris.cls('%REST.Impl')
13 |
14 | def get_from_module(app_path, app_module, app_name):
15 |
16 | # Add the path to the application
17 | if (app_path not in sys.path) :
18 | sys.path.append(app_path)
19 |
20 | # retrieve the application
21 | return getattr(importlib.import_module(app_module), app_name)
22 |
23 | # Changes the current working directory to the manager directory of the instance.
24 | def goto_manager_dir():
25 | iris.system.Process.CurrentDirectory(iris.system.Util.ManagerDirectory())
26 |
27 | def unicode_to_wsgi(u):
28 | # Convert an environment variable to a WSGI "bytes-as-unicode" string
29 | return u.encode(enc, esc).decode('iso-8859-1')
30 |
31 | def wsgi_to_bytes(s):
32 | return s.encode('iso-8859-1')
33 |
34 | def write(chunk):
35 | rest_service._WriteResponse(chunk)
36 |
37 | def start_response(status, response_headers, exc_info=None):
38 | '''WSGI start_response callable'''
39 | if exc_info:
40 | try:
41 | raise exc_info[1].with_traceback(exc_info[2])
42 | finally:
43 | exc_info = None
44 |
45 | rest_service._SetStatusCode(status)
46 | for tuple in response_headers:
47 | rest_service._SetHeader(tuple[0], tuple[1])
48 | return write
49 |
50 |
51 | # Make request to application
52 | def make_request(environ, stream, application, path):
53 |
54 | # Change the working directory for logging purposes
55 | goto_manager_dir()
56 |
57 | error_log_file = open('WSGI.log', 'a+')
58 |
59 | # We want the working directory to be the app's directory
60 | if (not path.endswith(os.path.sep)):
61 | path = path + os.path.sep
62 |
63 | #iris.system.Process.CurrentDirectory(path)
64 |
65 | # Set up the body of the request
66 | if stream != '':
67 | bytestream = stream
68 | elif (environ['CONTENT_TYPE'] == 'application/x-www-form-urlencoded'):
69 | bytestream = BytesIO()
70 | part = urllib.parse.urlencode(environ['formdata'])
71 | bytestream.write(part.encode('utf-8'))
72 | bytestream.seek(0)
73 | else:
74 | bytestream = BytesIO(b'')
75 |
76 | #for k,v in os.environ.items():
77 | #environ[k] = unicode_to_wsgi(v)
78 | environ['wsgi.input'] = bytestream
79 | environ['wsgi.errors'] = error_log_file
80 | environ['wsgi.version'] = (1, 0)
81 | environ['wsgi.multithread'] = False
82 | environ['wsgi.multiprocess'] = True
83 | environ['wsgi.run_once'] = True
84 |
85 |
86 | if environ.get('HTTPS', 'off') in ('on', '1'):
87 | environ['wsgi.url_scheme'] = 'https'
88 | else:
89 | environ['wsgi.url_scheme'] = 'http'
90 |
91 | # Calling WSGI application
92 | response = application(environ, start_response)
93 |
94 | error_log_file.close()
95 |
96 | try:
97 | for data in response:
98 | if data:
99 | # (REST.Impl).Write() needs a utf-8 string
100 | write(data.decode('utf-8'))
101 | write(b'')
102 | finally:
103 | if hasattr(response, 'close'):
104 | response.close()
105 |
--------------------------------------------------------------------------------
/src/iop/__init__.py:
--------------------------------------------------------------------------------
1 | from iop._business_operation import _BusinessOperation
2 | from iop._business_process import _BusinessProcess
3 | from iop._business_service import _BusinessService
4 | from iop._director import _Director
5 | from iop._inbound_adapter import _InboundAdapter
6 | from iop._message import _Message, _PickleMessage, _PydanticMessage, _PydanticPickleMessage
7 | from iop._outbound_adapter import _OutboundAdapter
8 | from iop._private_session_duplex import _PrivateSessionDuplex
9 | from iop._private_session_process import _PrivateSessionProcess
10 | from iop._utils import _Utils
11 |
12 | class Utils(_Utils): pass
13 | class InboundAdapter(_InboundAdapter): pass
14 | class OutboundAdapter(_OutboundAdapter): pass
15 | class BusinessService(_BusinessService): pass
16 | class BusinessOperation(_BusinessOperation): pass
17 | class BusinessProcess(_BusinessProcess): pass
18 | class DuplexService(_PrivateSessionDuplex): pass
19 | class DuplexOperation(_PrivateSessionDuplex): pass
20 | class DuplexProcess(_PrivateSessionProcess): pass
21 | class Message(_Message): pass
22 | class PickleMessage(_PickleMessage): pass
23 | class PydanticMessage(_PydanticMessage): pass
24 | class PydanticPickleMessage(_PydanticPickleMessage): pass
25 | class Director(_Director): pass
26 |
--------------------------------------------------------------------------------
/src/iop/__main__.py:
--------------------------------------------------------------------------------
1 | # main entry is _cli.main()
2 | if __name__ == '__main__':
3 | import iop._cli as _cli
4 | _cli.main()
--------------------------------------------------------------------------------
/src/iop/_async_request.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from typing import Any, Optional, Union
3 |
4 | from . import _iris
5 | from ._dispatch import dispatch_deserializer, dispatch_serializer
6 | from ._message import _Message as Message
7 |
8 | class AsyncRequest(asyncio.Future):
9 | _message_header_id: int = 0
10 | _queue_name: str = ""
11 | _end_time: int = 0
12 | _response: Any = None
13 | _done: bool = False
14 |
15 | def __init__(self, target: str, request: Union[Message, Any],
16 | timeout: int = -1, description: Optional[str] = None, host: Optional[Any] = None) -> None:
17 | super().__init__()
18 | self.target = target
19 | self.request = request
20 | self.timeout = timeout
21 | self.description = description
22 | self.host = host
23 | if host is None:
24 | raise ValueError("host parameter cannot be None")
25 | self._iris_handle = host.iris_handle
26 | asyncio.create_task(self.send())
27 |
28 | async def send(self) -> None:
29 | # init parameters
30 | iris = _iris.get_iris()
31 | message_header_id = iris.ref()
32 | queue_name = iris.ref()
33 | end_time = iris.ref()
34 | request = dispatch_serializer(self.request)
35 |
36 | # send request
37 | self._iris_handle.dispatchSendRequestAsyncNG(
38 | self.target, request, self.timeout, self.description,
39 | message_header_id, queue_name, end_time)
40 |
41 | # get byref values
42 | self._message_header_id = message_header_id.value
43 | self._queue_name = queue_name.value
44 | self._end_time = end_time.value
45 |
46 | while not self._done:
47 | await asyncio.sleep(0.1)
48 | self.is_done()
49 |
50 | self.set_result(self._response)
51 |
52 | def is_done(self) -> None:
53 | iris = _iris.get_iris()
54 | response = iris.ref()
55 | status = self._iris_handle.dispatchIsRequestDone(self.timeout, self._end_time,
56 | self._queue_name, self._message_header_id,
57 | response)
58 |
59 | self._response = dispatch_deserializer(response.value)
60 |
61 | if status == 2: # message found
62 | self._done = True
63 | elif status == 1: # message not found
64 | pass
65 | else:
66 | self._done = True
67 | self.set_exception(RuntimeError(iris.system.Status.GetOneStatusText(status)))
68 |
--------------------------------------------------------------------------------
/src/iop/_business_operation.py:
--------------------------------------------------------------------------------
1 | import importlib
2 | from typing import Any, List, Optional, Union, Tuple
3 |
4 | from ._business_host import _BusinessHost
5 | from ._decorators import input_deserializer, output_serializer, input_serializer, output_deserializer
6 | from ._dispatch import create_dispatch, dispach_message
7 |
8 | class _BusinessOperation(_BusinessHost):
9 | """Business operation component that handles outbound communication.
10 |
11 | Responsible for sending messages to external systems. Can optionally use an
12 | adapter to handle the outbound messaging protocol.
13 | """
14 |
15 | DISPATCH: List[Tuple[str, str]] = []
16 | Adapter: Any = None
17 | adapter: Any = None
18 |
19 | def on_message(self, request: Any) -> Any:
20 | """Handle incoming messages.
21 |
22 | Process messages received from other production components and either
23 | send to external system or forward to another component.
24 |
25 | Args:
26 | request: The incoming message
27 |
28 | Returns:
29 | Response message
30 | """
31 | return self.OnMessage(request)
32 |
33 | def on_keepalive(self) -> None:
34 | """
35 | Called when the server sends a keepalive message.
36 | """
37 | return
38 |
39 | def _set_iris_handles(self, handle_current: Any, handle_partner: Any) -> None:
40 | """For internal use only."""
41 | self.iris_handle = handle_current
42 | if type(handle_partner).__module__.find('iris') == 0:
43 | if handle_partner._IsA("Grongier.PEX.OutboundAdapter") or handle_partner._IsA("IOP.OutboundAdapter"):
44 | module = importlib.import_module(handle_partner.GetModule())
45 | handle_partner = getattr(module, handle_partner.GetClassname())()
46 | self.Adapter = self.adapter = handle_partner
47 | return
48 |
49 | def _dispatch_on_init(self, host_object: Any) -> None:
50 | """For internal use only."""
51 | create_dispatch(self)
52 | self.on_init()
53 | return
54 |
55 | @input_deserializer
56 | @output_serializer
57 | def _dispatch_on_message(self, request: Any) -> Any:
58 | """For internal use only."""
59 | return dispach_message(self,request)
60 |
61 | def OnMessage(self, request: Any) -> Any:
62 | """DEPRECATED : use on_message
63 | Called when the business operation receives a message from another production component.
64 | Typically, the operation will either send the message to the external system or forward it to a business process or another business operation.
65 | If the operation has an adapter, it uses the Adapter.invoke() method to call the method on the adapter that sends the message to the external system.
66 | If the operation is forwarding the message to another production component, it uses the SendRequestAsync() or the SendRequestSync() method
67 |
68 | Parameters:
69 | request: An instance of either a subclass of Message or of IRISObject containing the incoming message for the business operation.
70 |
71 | Returns:
72 | The response object
73 | """
74 | return
75 |
--------------------------------------------------------------------------------
/src/iop/_business_service.py:
--------------------------------------------------------------------------------
1 | import importlib
2 |
3 | from ._business_host import _BusinessHost
4 | from ._decorators import input_deserializer, output_serializer, input_serializer, output_deserializer
5 |
6 | class _BusinessService(_BusinessHost):
7 | """ This class is responsible for receiving the data from external system and sending it to business processes or business operations in the production.
8 | The business service can use an adapter to access the external system, which is specified in the InboundAdapter property.
9 | There are three ways of implementing a business service:
10 | 1) Polling business service with an adapter - The production framework at regular intervals calls the adapter’s OnTask() method,
11 | which sends the incoming data to the the business service ProcessInput() method, which, in turn calls the OnProcessInput method with your code.
12 | 2) Polling business service that uses the default adapter - In this case, the framework calls the default adapter's OnTask method with no data.
13 | The OnProcessInput() method then performs the role of the adapter and is responsible for accessing the external system and receiving the data.
14 | 3) Nonpolling business service - The production framework does not initiate the business service. Instead custom code in either a long-running process
15 | or one that is started at regular intervals initiates the business service by calling the Director.CreateBusinessService() method.
16 | """
17 | Adapter = adapter = None
18 | _wait_for_next_call_interval = False
19 |
20 | def _dispatch_on_init(self, host_object) -> None:
21 | """For internal use only."""
22 | self.on_init()
23 |
24 | return
25 |
26 | def on_process_input(self, message_input):
27 | """ Receives the message from the inbond adapter via the PRocessInput method and is responsible for forwarding it to target business processes or operations.
28 | If the business service does not specify an adapter, then the default adapter calls this method with no message
29 | and the business service is responsible for receiving the data from the external system and validating it.
30 |
31 | Parameters:
32 | message_input: an instance of IRISObject or subclass of Message containing the data that the inbound adapter passes in.
33 | The message can have any structure agreed upon by the inbound adapter and the business service.
34 | """
35 | return self.OnProcessInput(message_input)
36 |
37 | def _set_iris_handles(self, handle_current, handle_partner):
38 | """ For internal use only. """
39 | self.iris_handle = handle_current
40 | if type(handle_partner).__module__.find('iris') == 0:
41 | if handle_partner._IsA("Grongier.PEX.InboundAdapter") or handle_partner._IsA("IOP.InboundAdapter"):
42 | module = importlib.import_module(handle_partner.GetModule())
43 | handle_partner = getattr(module, handle_partner.GetClassname())()
44 | self.Adapter = self.adapter = handle_partner
45 | return
46 |
47 | @input_deserializer
48 | @output_serializer
49 | def _dispatch_on_process_input(self, request):
50 | """ For internal use only. """
51 | return self.on_process_input(request)
52 |
53 | def OnProcessInput(self, message_input):
54 | """ DEPRECATED : use on_process_input
55 | Receives the message from the inbond adapter via the PRocessInput method and is responsible for forwarding it to target business processes or operations.
56 | If the business service does not specify an adapter, then the default adapter calls this method with no message
57 | and the business service is responsible for receiving the data from the external system and validating it.
58 |
59 | Parameters:
60 | messageInput: an instance of IRISObject or subclass of Message containing the data that the inbound adapter passes in.
61 | The message can have any structure agreed upon by the inbound adapter and the business service.
62 | """
63 | return
64 |
--------------------------------------------------------------------------------
/src/iop/_decorators.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 | from typing import Any, Callable
3 |
4 | from ._dispatch import dispatch_deserializer, dispatch_serializer
5 |
6 | def input_serializer(fonction: Callable) -> Callable:
7 | """Decorator that serializes all input arguments."""
8 | def _dispatch_serializer(self, *params: Any, **param2: Any) -> Any:
9 | serialized = [dispatch_serializer(param) for param in params]
10 | param2 = {key: dispatch_serializer(value) for key, value in param2.items()}
11 | return fonction(self, *serialized, **param2)
12 | return _dispatch_serializer
13 |
14 | def input_serializer_param(position: int, name: str) -> Callable:
15 | """Decorator that serializes specific parameter by position or name."""
16 | def _input_serializer_param(fonction: Callable) -> Callable:
17 | @wraps(fonction)
18 | def _dispatch_serializer(self, *params: Any, **param2: Any) -> Any:
19 | serialized = [
20 | dispatch_serializer(param) if i == position else param
21 | for i, param in enumerate(params)
22 | ]
23 | param2 = {
24 | key: dispatch_serializer(value) if key == name else value
25 | for key, value in param2.items()
26 | }
27 | return fonction(self, *serialized, **param2)
28 | return _dispatch_serializer
29 | return _input_serializer_param
30 |
31 | def output_deserializer(fonction: Callable) -> Callable:
32 | """Decorator that deserializes function output."""
33 | def _dispatch_deserializer(self, *params: Any, **param2: Any) -> Any:
34 | return dispatch_deserializer(fonction(self, *params, **param2))
35 | return _dispatch_deserializer
36 |
37 | def input_deserializer(fonction: Callable) -> Callable:
38 | """Decorator that deserializes all input arguments."""
39 | def _dispatch_deserializer(self, *params: Any, **param2: Any) -> Any:
40 | serialized = [dispatch_deserializer(param) for param in params]
41 | param2 = {key: dispatch_deserializer(value) for key, value in param2.items()}
42 | return fonction(self, *serialized, **param2)
43 | return _dispatch_deserializer
44 |
45 | def output_serializer(fonction: Callable) -> Callable:
46 | """Decorator that serializes function output."""
47 | def _dispatch_serializer(self, *params: Any, **param2: Any) -> Any:
48 | return dispatch_serializer(fonction(self, *params, **param2))
49 | return _dispatch_serializer
50 |
--------------------------------------------------------------------------------
/src/iop/_dispatch.py:
--------------------------------------------------------------------------------
1 | from inspect import signature, Parameter
2 | from typing import Any, List, Tuple, Callable
3 |
4 | from ._serialization import serialize_message, serialize_pickle_message, deserialize_message, deserialize_pickle_message
5 | from ._message_validator import is_message_instance, is_pickle_message_instance, is_iris_object_instance
6 |
7 | def dispatch_serializer(message: Any) -> Any:
8 | """Serializes the message based on its type.
9 |
10 | Args:
11 | message: The message to serialize
12 |
13 | Returns:
14 | The serialized message
15 |
16 | Raises:
17 | TypeError: If message is invalid type
18 | """
19 | if message is not None:
20 | if is_message_instance(message):
21 | return serialize_message(message)
22 | elif is_pickle_message_instance(message):
23 | return serialize_pickle_message(message)
24 | elif is_iris_object_instance(message):
25 | return message
26 |
27 | if message == "" or message is None:
28 | return message
29 |
30 | raise TypeError("The message must be an instance of a class that is a subclass of Message or IRISObject %Persistent class.")
31 |
32 | def dispatch_deserializer(serial: Any) -> Any:
33 | """Deserializes the message based on its type.
34 |
35 | Args:
36 | serial: The serialized message
37 |
38 | Returns:
39 | The deserialized message
40 | """
41 | if (
42 | serial is not None
43 | and type(serial).__module__.startswith('iris')
44 | and (
45 | serial._IsA("IOP.Message")
46 | or serial._IsA("Grongier.PEX.Message")
47 | )
48 | ):
49 | return deserialize_message(serial)
50 | elif (
51 | serial is not None
52 | and type(serial).__module__.startswith('iris')
53 | and (
54 | serial._IsA("IOP.PickleMessage")
55 | or serial._IsA("Grongier.PEX.PickleMessage")
56 | )
57 | ):
58 | return deserialize_pickle_message(serial)
59 | else:
60 | return serial
61 |
62 | def dispach_message(host: Any, request: Any) -> Any:
63 | """Dispatches the message to the appropriate method.
64 |
65 | Args:
66 | request: The request object
67 |
68 | Returns:
69 | The response object
70 | """
71 | call = 'on_message'
72 |
73 | module = request.__class__.__module__
74 | classname = request.__class__.__name__
75 |
76 | for msg, method in host.DISPATCH:
77 | if msg == module + "." + classname:
78 | call = method
79 |
80 | return getattr(host, call)(request)
81 |
82 | def create_dispatch(host: Any) -> None:
83 | """Creates a dispatch table mapping class names to their handler methods.
84 | The dispatch table consists of tuples of (fully_qualified_class_name, method_name).
85 | Only methods that take a single typed parameter are considered as handlers.
86 | """
87 | if len(host.DISPATCH) > 0:
88 | return
89 |
90 | for method_name in get_callable_methods(host):
91 | handler_info = get_handler_info(host, method_name)
92 | if handler_info:
93 | host.DISPATCH.append(handler_info)
94 |
95 | def get_callable_methods(host: Any) -> List[str]:
96 | """Returns a list of callable method names that don't start with underscore."""
97 | return [
98 | func for func in dir(host)
99 | if callable(getattr(host, func)) and not func.startswith("_")
100 | ]
101 |
102 | def get_handler_info(host: Any, method_name: str) -> Tuple[str, str] | None:
103 | """Analyzes a method to determine if it's a valid message handler.
104 | Returns a tuple of (fully_qualified_class_name, method_name) if valid,
105 | None otherwise.
106 | """
107 | try:
108 | params = signature(getattr(host, method_name)).parameters
109 | if len(params) != 1:
110 | return None
111 |
112 | param: Parameter = next(iter(params.values()))
113 | annotation = param.annotation
114 |
115 | if annotation == Parameter.empty or not isinstance(annotation, type):
116 | return None
117 |
118 | return f"{annotation.__module__}.{annotation.__name__}", method_name
119 |
120 | except ValueError:
121 | return None
--------------------------------------------------------------------------------
/src/iop/_inbound_adapter.py:
--------------------------------------------------------------------------------
1 | from ._common import _Common
2 |
3 | class _InboundAdapter(_Common):
4 | """ Responsible for receiving the data from the external system, validating the data,
5 | and sending it to the business service by calling the BusinessHost.ProcessInput() method.
6 | """
7 | BusinessHost = business_host = business_host_python = None
8 |
9 | def on_task(self):
10 | """ Called by the production framework at intervals determined by the business service CallInterval property.
11 | It is responsible for receiving the data from the external system, validating the data, and sending it in a message to the business service OnProcessInput method.
12 | The message can have any structure agreed upon by the inbound adapter and the business service.
13 | """
14 | return self.OnTask()
15 |
16 | def _set_iris_handles(self, handle_current, handle_partner):
17 | """ For internal use only. """
18 | self.iris_handle = handle_current
19 | self.BusinessHost = handle_partner
20 | self.business_host = handle_partner
21 | try:
22 | self.business_host_python = handle_partner.GetClass()
23 | except:
24 | pass
25 | return
26 |
27 |
28 | def OnTask(self):
29 | """ DEPRECATED : use on_task
30 | Called by the production framework at intervals determined by the business service CallInterval property.
31 | It is responsible for receiving the data from the external system, validating the data, and sending it in a message to the business service OnProcessInput method.
32 | The message can have any structure agreed upon by the inbound adapter and the business service.
33 | """
34 | return
--------------------------------------------------------------------------------
/src/iop/_iris.py:
--------------------------------------------------------------------------------
1 | import os
2 | from typing import Optional
3 |
4 | def get_iris(namespace: Optional[str]=None)->'iris': # type: ignore
5 | if namespace:
6 | os.environ['IRISNAMESPACE'] = namespace
7 | import iris
8 | return iris
--------------------------------------------------------------------------------
/src/iop/_log_manager.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from . import _iris
4 |
5 | class LogManager:
6 | """Manages logging integration between Python's logging module and IRIS."""
7 |
8 | @staticmethod
9 | def get_logger(class_name: str, to_console: bool = False) -> logging.Logger:
10 | """Get a logger instance configured for IRIS integration.
11 |
12 | Args:
13 | class_name: Name of the class logging the message
14 | method_name: Optional name of the method logging the message
15 | to_console: If True, log to the console instead of IRIS
16 |
17 | Returns:
18 | Logger instance configured for IRIS integration
19 | """
20 | logger = logging.Logger(f"{class_name}")
21 |
22 | # Only add handler if none exists
23 | if not logger.handlers:
24 | handler = IRISLogHandler(to_console=to_console)
25 | formatter = logging.Formatter('%(message)s')
26 | handler.setFormatter(formatter)
27 | logger.addHandler(handler)
28 | # Set the log level to the lowest level to ensure all messages are sent to IRIS
29 | logger.setLevel(logging.DEBUG)
30 |
31 | return logger
32 |
33 | class IRISLogHandler(logging.Handler):
34 | """Custom logging handler that routes Python logs to IRIS logging system."""
35 |
36 | def __init__(self, to_console: bool = False):
37 | """Initialize the IRIS logging handler."""
38 | super().__init__()
39 | self.to_console = to_console
40 |
41 | # Map Python logging levels to IRIS logging methods
42 | self.level_map = {
43 | logging.DEBUG: 5,
44 | logging.INFO: 4,
45 | logging.WARNING: 3,
46 | logging.ERROR: 2,
47 | logging.CRITICAL: 6,
48 | }
49 | # Map Python logging levels to IRIS logging Console level
50 | self.level_map_console = {
51 | logging.DEBUG: -1,
52 | logging.INFO: 0,
53 | logging.WARNING: 1,
54 | logging.ERROR: 2,
55 | logging.CRITICAL: 3,
56 | }
57 |
58 | def format(self, record: logging.LogRecord) -> str:
59 | """Format the log record into a string.
60 |
61 | Args:
62 | record: The logging record to format
63 |
64 | Returns:
65 | Formatted log message
66 | """
67 | return record.getMessage()
68 |
69 | def emit(self, record: logging.LogRecord) -> None:
70 | """Route the log record to appropriate IRIS logging method.
71 |
72 | Args:
73 | record: The logging record to emit
74 | """
75 | # Extract class and method names with fallbacks
76 | class_name = getattr(record, "class_name", record.name)
77 | method_name = getattr(record, "method_name", record.funcName)
78 |
79 | # Format message and get full method path
80 | message = self.format(record)
81 | method_path = f"{class_name}.{method_name}"
82 |
83 | # Determine if console logging should be used
84 | use_console = self.to_console or getattr(record, "to_console", False)
85 |
86 | if use_console:
87 | _iris.get_iris().cls("%SYS.System").WriteToConsoleLog(
88 | message,
89 | 0,
90 | self.level_map_console.get(record.levelno, 0),
91 | method_path
92 | )
93 | else:
94 | log_level = self.level_map.get(record.levelno, 4)
95 | _iris.get_iris().cls("Ens.Util.Log").Log(
96 | log_level,
97 | class_name,
98 | method_name,
99 | message
100 | )
101 |
--------------------------------------------------------------------------------
/src/iop/_message.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from pydantic import BaseModel
4 |
5 | class _Message:
6 | """ The abstract class that is the superclass for persistent messages sent from one component to another.
7 | This class has no properties or methods. Users subclass Message and add properties.
8 | The IOP framework provides the persistence to objects derived from the Message class.
9 | """
10 | pass
11 |
12 | class _PickleMessage:
13 | """ The abstract class that is the superclass for persistent messages sent from one component to another.
14 | This class has no properties or methods. Users subclass Message and add properties.
15 | The IOP framework provides the persistence to objects derived from the Message class.
16 | """
17 | pass
18 |
19 | class _PydanticMessage(BaseModel):
20 | """Base class for Pydantic-based messages that can be serialized to IRIS."""
21 |
22 | def __init__(self, **data: Any):
23 | super().__init__(**data)
24 |
25 | class _PydanticPickleMessage(BaseModel):
26 | """Base class for Pydantic-based messages that can be serialized to IRIS."""
27 |
28 | def __init__(self, **data: Any):
29 | super().__init__(**data)
--------------------------------------------------------------------------------
/src/iop/_message_validator.py:
--------------------------------------------------------------------------------
1 | import dataclasses
2 | from typing import Any, Type
3 |
4 | from ._message import _Message, _PickleMessage, _PydanticPickleMessage, BaseModel
5 |
6 |
7 | def is_message_instance(obj: Any) -> bool:
8 | """Check if object is a valid Message instance."""
9 | if isinstance(obj, BaseModel):
10 | return True
11 | if is_message_class(type(obj)):
12 | if not dataclasses.is_dataclass(obj):
13 | raise TypeError(f"{type(obj).__module__}.{type(obj).__qualname__} must be a dataclass")
14 | return True
15 | return False
16 |
17 |
18 | def is_pickle_message_instance(obj: Any) -> bool:
19 | """Check if object is a PickleMessage instance."""
20 | if isinstance(obj, _PydanticPickleMessage):
21 | return True
22 | if is_pickle_message_class(type(obj)):
23 | return True
24 | return False
25 |
26 |
27 | def is_iris_object_instance(obj: Any) -> bool:
28 | """Check if object is an IRIS persistent object."""
29 | return (obj is not None and
30 | type(obj).__module__.startswith('iris') and
31 | (obj._IsA("%Persistent") or obj._IsA("%Stream.Object")))
32 | # Stream.Object are used for HTTP InboundAdapter/OutboundAdapter
33 |
34 |
35 | def is_message_class(klass: Type) -> bool:
36 | """Check if class is a Message type."""
37 | if issubclass(klass, _Message):
38 | return True
39 | return False
40 |
41 |
42 |
43 | def is_pickle_message_class(klass: Type) -> bool:
44 | """Check if class is a PickleMessage type."""
45 | if issubclass(klass, _PickleMessage):
46 | return True
47 | if issubclass(klass, _PydanticPickleMessage):
48 | return True
49 | return False
50 |
--------------------------------------------------------------------------------
/src/iop/_outbound_adapter.py:
--------------------------------------------------------------------------------
1 | from ._common import _Common
2 |
3 | class _OutboundAdapter(_Common):
4 | """ Responsible for sending the data to the external system."""
5 | BusinessHost = business_host = business_host_python = None
6 |
7 | def on_keepalive(self):
8 | """
9 | > This function is called when the server sends a keepalive message
10 | """
11 | return
12 |
13 | def _set_iris_handles(self, handle_current, handle_partner):
14 | """ For internal use only. """
15 | self.iris_handle = handle_current
16 | self.BusinessHost = handle_partner
17 | self.business_host = handle_partner
18 | try:
19 | self.business_host_python = handle_partner.GetClass()
20 | except:
21 | pass
22 | return
23 |
24 |
--------------------------------------------------------------------------------
/src/iop/_private_session_process.py:
--------------------------------------------------------------------------------
1 | from ._business_process import _BusinessProcess
2 | from ._decorators import input_deserializer, output_serializer, input_serializer, output_deserializer
3 |
4 | class _PrivateSessionProcess(_BusinessProcess):
5 |
6 | @input_deserializer
7 | @output_serializer
8 | def _dispatch_on_document(self, host_object,source_config_name, request):
9 | """ For internal use only. """
10 | self._restore_persistent_properties(host_object)
11 | return_object = self.on_document(source_config_name,request)
12 | self._save_persistent_properties(host_object)
13 | return return_object
14 |
15 | def on_document(self,source_config_name,request):
16 | pass
17 |
18 |
19 | @input_deserializer
20 | @output_serializer
21 | def _dispatch_on_private_session_started(self, host_object, source_config_name,self_generated):
22 | """ For internal use only. """
23 | self._restore_persistent_properties(host_object)
24 | return_object = self.on_private_session_started(source_config_name,self_generated)
25 | self._save_persistent_properties(host_object)
26 | return return_object
27 |
28 | def on_private_session_started(self,source_config_name,self_generated):
29 | pass
30 |
31 | @input_deserializer
32 | @output_serializer
33 | def _dispatch_on_private_session_stopped(self, host_object, source_config_name,self_generated,message):
34 | """ For internal use only. """
35 | self._restore_persistent_properties(host_object)
36 | return_object = self.on_private_session_stopped(source_config_name,self_generated,message)
37 | self._save_persistent_properties(host_object)
38 | return return_object
39 |
40 | def on_private_session_stopped(self,source_config_name,self_generated,message):
41 | pass
--------------------------------------------------------------------------------
/src/iop/cls/IOP/BusinessOperation.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2021 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Class IOP.BusinessOperation Extends (Ens.BusinessOperation, IOP.Common) [ Inheritance = right, ProcedureBlock, System = 4 ]
6 | {
7 |
8 | Parameter SETTINGS = "%classname:Python BusinessOperation,%module:Python BusinessOperation,%settings:Python BusinessOperation,%classpaths:Python BusinessOperation";
9 |
10 | Method OnMessage(
11 | request As %Library.Persistent,
12 | Output response As %Library.Persistent) As %Status
13 | {
14 | set tSC = $$$OK
15 | try {
16 | set response = ..%class."_dispatch_on_message"(request)
17 | } catch ex {
18 | set tSC = ..DisplayTraceback(ex)
19 | }
20 | quit tSC
21 | }
22 |
23 | Method OnKeepalive(pStatus As %Status = {$$$OK}) As %Status
24 | {
25 | set tSC = $$$OK
26 | try {
27 | $$$ThrowOnError(##super(pStatus))
28 | do ..%class."on_keepalive"()
29 | } catch ex {
30 | set tSC = ..DisplayTraceback(ex)
31 | }
32 | quit tSC
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/iop/cls/IOP/BusinessProcess.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2021 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Class IOP.BusinessProcess Extends (Ens.BusinessProcess, IOP.Common) [ Inheritance = right, ProcedureBlock, System = 4 ]
6 | {
7 |
8 | Parameter SETTINGS = "%classname:Python BusinessProcess,%module:Python BusinessProcess,%settings:Python BusinessProcess,%classpaths:Python BusinessProcess";
9 |
10 | Property persistentProperties As array Of %String(MAXLEN = "");
11 |
12 | Method dispatchReply(response)
13 | {
14 | set tSC = ..Reply(response)
15 | if $$$ISERR(tSC) throw ##class(%Exception.StatusException).CreateFromStatus(tSC)
16 | quit
17 | }
18 |
19 | Method dispatchSetTimer(
20 | timeout,
21 | completionKey)
22 | {
23 | set tSC = ..SetTimer(timeout,$g(completionKey))
24 | if $$$ISERR(tSC) throw ##class(%Exception.StatusException).CreateFromStatus(tSC)
25 | quit
26 | }
27 |
28 | Method dispatchSendRequestAsync(
29 | target,
30 | request,
31 | responseRequired,
32 | completionKey,
33 | description)
34 | {
35 | Try {
36 | $$$ThrowOnError(..SendRequestAsync(target,request,responseRequired,completionKey,description))
37 | }
38 | Catch ex {
39 | set tSC = ..DisplayTraceback(ex)
40 | }
41 |
42 | quit
43 | }
44 |
45 | Method OnRequest(
46 | request As %Persistent,
47 | Output response As %Persistent) As %Status
48 | {
49 | set tSC = $$$OK
50 | try {
51 | set response = ..%class."_dispatch_on_request"($this,request)
52 | } catch ex {
53 | set tSC = ..DisplayTraceback(ex)
54 | }
55 | quit tSC
56 | }
57 |
58 | /// Handle a 'Response'
59 | Method OnResponse(
60 | request As %Persistent,
61 | Output response As %Persistent,
62 | callRequest As %Persistent,
63 | callResponse As %Persistent,
64 | pCompletionKey As %String) As %Status
65 | {
66 | set tSC = $$$OK
67 | try {
68 | set response = ..%class."_dispatch_on_response"($this,request,response,callRequest,callResponse,pCompletionKey)
69 | } catch ex {
70 | set tSC = ..DisplayTraceback(ex)
71 | }
72 | quit tSC
73 | }
74 |
75 | Method OnComplete(
76 | request As %Library.Persistent,
77 | ByRef response As %Library.Persistent) As %Status
78 | {
79 | set tSC = $$$OK
80 | try {
81 | set response = ..%class."_dispatch_on_complete"($this,request,response)
82 | } catch ex {
83 | set tSC = ..DisplayTraceback(ex)
84 | }
85 | quit tSC
86 | }
87 |
88 | Method getPersistentProperty(name)
89 | {
90 | quit ..persistentProperties.GetAt(name)
91 | }
92 |
93 | Method setPersistentProperty(
94 | name,
95 | value)
96 | {
97 | quit ..persistentProperties.SetAt(value,name)
98 | }
99 |
100 | Storage Default
101 | {
102 |
103 | "BusinessProcess"
104 |
105 | %classpaths
106 |
107 |
108 | %classname
109 |
110 |
111 | %module
112 |
113 |
114 | %settings
115 |
116 |
117 | %class
118 |
119 |
120 | %enable
121 |
122 |
123 | %timeout
124 |
125 |
126 | %port
127 |
128 |
129 | %PythonInterpreterPath
130 |
131 |
132 | %traceback
133 |
134 |
135 |
136 | persistentProperties
137 | subnode
138 | "IOP.BusinessProcess.persistentProperties"
139 |
140 | BusinessProcessDefaultData1
141 | %Storage.Persistent
142 | }
143 |
144 | }
145 |
--------------------------------------------------------------------------------
/src/iop/cls/IOP/BusinessService.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2021 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Class IOP.BusinessService Extends (Ens.BusinessService, IOP.Common) [ Inheritance = right, ProcedureBlock, System = 4 ]
6 | {
7 |
8 | Parameter SETTINGS = "%classname:Python BusinessService,%module:Python BusinessService,%settings:Python BusinessService,%classpaths:Python BusinessService";
9 |
10 | Method dispatchProcessInput(pInput As %RegisteredObject) As %RegisteredObject
11 | {
12 | try {
13 | set response = ..%class."_dispatch_on_process_input"(pInput)
14 | } catch ex {
15 | set tSC = ..DisplayTraceback(ex)
16 | throw ##class(%Exception.StatusException).CreateFromStatus(tSC)
17 | }
18 | quit response
19 | }
20 |
21 | Method OnProcessInput(
22 | request As %RegisteredObject,
23 | Output response As %RegisteredObject) As %Status
24 | {
25 | set tSC = $$$OK
26 | try {
27 | try {
28 | set ..%class."_wait_for_next_call_interval" = ..%WaitForNextCallInterval
29 | } catch {}
30 | set response = ..%class."_dispatch_on_process_input"(request)
31 | try {
32 | set ..%WaitForNextCallInterval = ..%class."_wait_for_next_call_interval"
33 | } catch {}
34 | } catch ex {
35 | set tSC = ..DisplayTraceback(ex)
36 | }
37 | quit tSC
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/src/iop/cls/IOP/Director.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2021 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Include (%occInclude, Ensemble)
6 |
7 | Class IOP.Director [ Inheritance = right, ProcedureBlock, System = 4 ]
8 | {
9 |
10 | ClassMethod dispatchCreateBusinessService(pTargetDispatchName As %String) As Ens.BusinessService
11 | {
12 | set tSC = ##class(Ens.Director).CreateBusinessService(pTargetDispatchName,.service)
13 |
14 | // Hack to prevent job to be registered in the production
15 | do ##class(Ens.Job).UnRegister(pTargetDispatchName,$JOB)
16 |
17 | if $$$ISERR(tSC) throw ##class(%Exception.StatusException).CreateFromStatus(tSC)
18 |
19 | quit service
20 | }
21 |
22 | ClassMethod dispatchListProductions() As %String
23 | {
24 | // Loop over the productions in this namespace
25 | Set tRS = ##class(%ResultSet).%New("Ens.Config.Production:ProductionStatus")
26 | If '$IsObject(tRS) Set tSC = %objlasterror Quit
27 |
28 | Set tSC = tRS.Execute()
29 | Quit:$$$ISERR(tSC)
30 |
31 | set tDict = ##class(%SYS.Python).Import("builtins").dict()
32 |
33 | While (tRS.Next()) {
34 | Set tProduction = tRS.Data("Production")
35 | Set tInfo = ##class(%SYS.Python).Import("builtins").dict()
36 | do tInfo."__setitem__"("Status",tRS.Data("Status"))
37 | do tInfo."__setitem__"("LastStartTime",tRS.Data("LastStartTime"))
38 | do tInfo."__setitem__"("LastStopTime",tRS.Data("LastStopTime"))
39 | do tInfo."__setitem__"("AutoStart",$G(^Ens.AutoStart)=tProduction)
40 | do tDict."__setitem__"(tProduction,tInfo)
41 | }
42 |
43 | Kill tRS
44 |
45 | return tDict
46 | }
47 |
48 | ClassMethod StatusProduction() As %String
49 | {
50 | Set sc = $$$OK
51 | Set tInfo = ##class(%SYS.Python).Import("builtins").dict()
52 | $$$ThrowOnError(##class(Ens.Director).GetProductionStatus(.tProdName,.tStatus))
53 | do tInfo."__setitem__"("Production",tProdName)
54 | do tInfo."__setitem__"("Status",$CASE(tStatus,$$$eProductionStateRunning:"running",
55 | $$$eProductionStateStopped:"stopped",
56 | $$$eProductionStateSuspended:"suspended",
57 | $$$eProductionStateTroubled:"toubled",
58 | :"unknown"))
59 | Return tInfo
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/src/iop/cls/IOP/Duplex/Operation.cls:
--------------------------------------------------------------------------------
1 | Class IOP.DuplexOperation Extends IOP.PrivateSessionDuplex
2 | {
3 |
4 | ClassMethod OnBusinessType(pItem As Ens.Config.Item) As %Integer
5 | {
6 | Quit $$$eHostTypeOperation
7 | }
8 |
9 | XData MessageMap
10 | {
11 |
12 | OnMessage
13 |
14 | }
15 |
16 | Method OnMessage(
17 | request As %Library.Persistent,
18 | Output response As %Library.Persistent) As %Status
19 | {
20 | set tSC = $$$OK
21 | try {
22 | set response = ..%class."_dispatch_on_message"(request)
23 | } catch ex {
24 | set tSC = ex.AsStatus()
25 | }
26 | quit tSC
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/iop/cls/IOP/Duplex/Service.cls:
--------------------------------------------------------------------------------
1 | Class IOP.DuplexService Extends IOP.PrivateSessionDuplex
2 | {
3 |
4 | ClassMethod OnBusinessType(pItem As Ens.Config.Item) As %Integer
5 | {
6 | Quit $$$eHostTypeService
7 | }
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/src/iop/cls/IOP/InboundAdapter.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2021 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Class IOP.InboundAdapter Extends (Ens.InboundAdapter, IOP.Common) [ Inheritance = right, ProcedureBlock, System = 4 ]
6 | {
7 |
8 | Parameter SETTINGS = "%classname:Python InboundAdapter,%module:Python InboundAdapter,%settings:Python InboundAdapter,%classpaths:Python InboundAdapter";
9 |
10 | Method OnTask() As %Status
11 | {
12 | set tSC = $$$OK
13 | try {
14 | $$$ThrowOnError(..Connect())
15 | do ..%class."on_task"()
16 | } catch ex {
17 | set tSC = ..DisplayTraceback(ex)
18 | }
19 | quit tSC
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/src/iop/cls/IOP/Message/JSONSchema.cls:
--------------------------------------------------------------------------------
1 | Class IOP.Message.JSONSchema Extends %Persistent
2 | {
3 |
4 | Property Name As %String;
5 |
6 | Property Category As %String;
7 |
8 | Property JSONSchema As %String(MAXLEN = "");
9 |
10 | Index NameIndex On Name [ IdKey, Unique ];
11 |
12 | /// Import a JSON Schema from file
13 | ClassMethod ImportFromFile(
14 | pFileName As %String,
15 | pCategory As %String = "",
16 | pName As %String) As %Status
17 | {
18 | Set pStatus = $$$OK
19 | Try {
20 | If '##class(%File).Exists(pFileName) {
21 | Set pStatus = $$$ERROR($$$GeneralError, "File not found")
22 | Return pStatus
23 | }
24 | Set tFile = ##class(%File).%New(pFileName)
25 | $$$ThrowOnError(tFile.Open("R"))
26 | Set tSchema = ""
27 | While 'tFile.AtEnd {
28 | Set tSchema = tSchema _ tFile.ReadLine()
29 | }
30 | Set pStatus = ..Import(tSchema, pCategory, pName)
31 | } Catch ex {
32 | Set pStatus = ex.AsStatus()
33 | }
34 | Return pStatus
35 | }
36 |
37 | /// Store the JSON Schema in this object
38 | ClassMethod Import(
39 | pSchema As %String,
40 | pCategory As %String = "",
41 | pName As %String) As %Status
42 | {
43 | Set pStatus = $$$OK
44 | Try {
45 | if ##class(IOP.Message.JSONSchema).%ExistsId(pName) {
46 | Set tThis = ##class(IOP.Message.JSONSchema).%OpenId(pName)
47 | Set tThis.Category = pCategory
48 | Set tThis.JSONSchema = pSchema
49 | $$$ThrowOnError(tThis.%Save())
50 | } Else {
51 | Set tThis = ##class(IOP.Message.JSONSchema).%New()
52 | Set tThis.Name = pName
53 | Set tThis.Category = pCategory
54 | Set tThis.JSONSchema = pSchema
55 | $$$ThrowOnError(tThis.%Save())
56 | }
57 | } Catch ex {
58 | Set pStatus = ex.AsStatus()
59 | }
60 | Quit pStatus
61 | }
62 |
63 | /// Get a stored schema by category and name
64 | ClassMethod GetSchema(
65 | pName As %String = "",
66 | Output pSchema As %String) As %Status
67 | {
68 | Set pStatus = $$$OK
69 | Try {
70 | Set tSql = "SELECT JSONSchema FROM IOP_Message.JSONSchema WHERE Name = ?"
71 | Set tStatement = ##class(%SQL.Statement).%New()
72 | Do tStatement.%Prepare(tSql)
73 | set rs = tStatement.%Execute(pName)
74 | If rs.%Next() {
75 | Set pSchema = rs.%Get("JSONSchema")
76 | } Else {
77 | Set pStatus = $$$ERROR($$$GeneralError, "Schema not found")
78 | }
79 | } Catch ex {
80 | Set pStatus = ex.AsStatus()
81 | }
82 | Return pStatus
83 | }
84 |
85 | /// Validate JSON data against a stored schema
86 | ClassMethod ValidateJSONSchema(
87 | pJSON As %String,
88 | pName As %String) As %Status
89 | {
90 | Set tSC = $$$OK
91 | Try {
92 | Set tSchema = ""
93 | Set tSC = ..GetSchema(pName, .tSchema)
94 | If $$$ISERR(tSC) Return tSC
95 | // Validate JSON data against schema
96 | // To be implemented
97 | Set tSC = $$$OK
98 | } Catch ex {
99 | Set tSC = ex.AsStatus()
100 | }
101 | Return tSC
102 | }
103 |
104 | Storage Default
105 | {
106 |
107 |
108 | %%CLASSNAME
109 |
110 |
111 | JSONSchema
112 |
113 |
114 | Category
115 |
116 |
117 | ^IOP.Message.JSONSchemaD
118 | JSONSchemaDefaultData
119 | ^IOP.Message.JSONSchemaD
120 | ^IOP.Message.JSONSchemaI
121 | ^IOP.Message.JSONSchemaS
122 | %Storage.Persistent
123 | }
124 |
125 | }
126 |
--------------------------------------------------------------------------------
/src/iop/cls/IOP/OutboundAdapter.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2021 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Class IOP.OutboundAdapter Extends (Ens.OutboundAdapter, IOP.Common) [ Inheritance = right, ProcedureBlock, System = 4 ]
6 | {
7 |
8 | Property KeepaliveInterval As %Numeric [ InitialExpression = 0 ];
9 |
10 | Parameter SETTINGS = "KeepaliveInterval:Python CallInterval,%classname:Python OutboundAdapter,%module:Python OutboundAdapter,%settings:Python OutboundAdapter,%classpaths:Python OutboundAdapter";
11 |
12 | Method %DispatchMethod(
13 | method As %String,
14 | args...) As %ObjectHandle
15 | {
16 | if $quit {
17 | quit $method($this.%class,method,args...)
18 | } else {
19 | do $method($this.%class,method,args...)
20 | quit
21 | }
22 | }
23 |
24 | Method OnKeepalive(pStatus As %Status = {$$$OK}) As %Status
25 | {
26 | set tSC = $$$OK
27 | try {
28 | $$$ThrowOnError(##super(pStatus))
29 | do ..%class."on_keepalive"()
30 | } catch ex {
31 | set tSC = ..DisplayTraceback(ex)
32 | }
33 | quit tSC
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/src/iop/cls/IOP/PickleMessage.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2021 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | Class IOP.PickleMessage Extends (Ens.MessageBody, %CSP.Page)
6 | {
7 |
8 | Property classname As %String(MAXLEN = "");
9 |
10 | Property jstr As %Stream.GlobalCharacter [ Internal, Private ];
11 |
12 | Method %OnNew(classname) As %Status [ Private, ServerOnly = 1 ]
13 | {
14 | set ..classname = $g(classname)
15 | Quit $$$OK
16 | }
17 |
18 | /// This method is called by the Management Portal to determine the content type that will be returned by the %ShowContents method.
19 | /// The return value is a string containing an HTTP content type.
20 | Method %GetContentType() As %String
21 | {
22 | Quit "text/html"
23 | }
24 |
25 | /// This method is called by the Management Portal to display a message-specific content viewer.
26 | /// This method displays its content by writing out to the current device.
27 | /// The content should match the type returned by the %GetContentType method.
28 | Method %ShowContents(pZenOutput As %Boolean = 0)
29 | {
30 | // https://github.com/bazh/jquery.json-view
31 | &html<#(..classname)#
>
32 | &html<Pickle Pyhton Message can't be displayed
>
33 | }
34 |
35 | Storage Default
36 | {
37 |
38 | "Message"
39 |
40 | classname
41 |
42 |
43 | json
44 |
45 |
46 | jstr
47 |
48 |
49 |
50 | jsonObject
51 | node
52 | "IOP.Message.jsonObject"
53 |
54 | MessageDefaultData
55 | %Storage.Persistent
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/src/iop/cls/IOP/PrivateSession/Message/Ack.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2022 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | /// This class is a DICOM framework class
6 | Class IOP.PrivateSession.Message.Ack Extends (%Persistent, Ens.Util.MessageBodyMethods) [ ClassType = persistent, Inheritance = right, ProcedureBlock, System = 4 ]
7 | {
8 |
9 | Parameter DOMAIN = "PrivateSession";
10 |
11 | /// From 'Ens.Util.MessageBodyMethods'
12 | Method %ShowContents(pZenOutput As %Boolean = 0)
13 | {
14 | Write $$$Text("(session-ack)")
15 | }
16 |
17 | Storage Default
18 | {
19 |
20 |
21 | %%CLASSNAME
22 |
23 |
24 | ^IOP.PrivateSe9756.AckD
25 | AckDefaultData
26 | ^IOP.PrivateSe9756.AckD
27 | ^IOP.PrivateSe9756.AckI
28 | ^IOP.PrivateSe9756.AckS
29 | %Storage.Persistent
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/iop/cls/IOP/PrivateSession/Message/Poll.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2022 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | /// This class is a DICOM framework class
6 | Class IOP.PrivateSession.Message.Poll Extends (%Persistent, Ens.Util.MessageBodyMethods) [ ClassType = persistent, Inheritance = right, ProcedureBlock, System = 4 ]
7 | {
8 |
9 | Parameter DOMAIN = "PrivateSession";
10 |
11 | /// From 'Ens.Util.MessageBodyMethods'
12 | Method %ShowContents(pZenOutput As %Boolean = 0)
13 | {
14 | Write $$$Text("(poll-data)")
15 | }
16 |
17 | Storage Default
18 | {
19 |
20 |
21 | %%CLASSNAME
22 |
23 |
24 | ^IOP.PrivateS9756.PollD
25 | PollDefaultData
26 | ^IOP.PrivateS9756.PollD
27 | ^IOP.PrivateS9756.PollI
28 | ^IOP.PrivateS9756.PollS
29 | %Storage.Persistent
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/iop/cls/IOP/PrivateSession/Message/Start.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2022 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | /// This class is a DICOM framework class
6 | Class IOP.PrivateSession.Message.Start Extends (%Persistent, Ens.Util.MessageBodyMethods) [ ClassType = persistent, Inheritance = right, ProcedureBlock, System = 4 ]
7 | {
8 |
9 | Parameter DOMAIN = "PrivateSession";
10 |
11 | /// From 'Ens.Util.MessageBodyMethods'
12 | Method %ShowContents(pZenOutput As %Boolean = 0)
13 | {
14 | Write $$$Text("(session-start)")
15 | }
16 |
17 | Storage Default
18 | {
19 |
20 |
21 | %%CLASSNAME
22 |
23 |
24 | ^IOP.Private9756.StartD
25 | StartDefaultData
26 | ^IOP.Private9756.StartD
27 | ^IOP.Private9756.StartI
28 | ^IOP.Private9756.StartS
29 | %Storage.Persistent
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/iop/cls/IOP/PrivateSession/Message/Stop.cls:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2022 by InterSystems Corporation.
2 | Cambridge, Massachusetts, U.S.A. All rights reserved.
3 | Confidential property of InterSystems Corporation. */
4 |
5 | /// This class is a DICOM framework class
6 | Class IOP.PrivateSession.Message.Stop Extends (%Persistent, Ens.Util.MessageBodyMethods) [ ClassType = persistent, Inheritance = right, ProcedureBlock, System = 4 ]
7 | {
8 |
9 | Parameter DOMAIN = "PrivateSession";
10 |
11 | /// The message body
12 | Property AttachedMessage As %Persistent(CLASSNAME = 1);
13 |
14 | /// From 'Ens.Util.MessageBodyMethods'
15 | Method %ShowContents(pZenOutput As %Boolean = 0)
16 | {
17 | If $IsObject(..AttachedMessage) {
18 | Write $$$FormatText($$$Text("(session-stop) with AttachedMessage [%1] "),$classname(..AttachedMessage))
19 | } Else {
20 | Write $$$Text("(session-stop)")
21 | }
22 | }
23 |
24 | Method %OnNew(initvalue As %RegisteredObject) As %Status [ Private, ProcedureBlock = 1, ServerOnly = 1 ]
25 | {
26 | Set ..AttachedMessage=initvalue
27 | Quit $$$OK
28 | }
29 |
30 | Storage Default
31 | {
32 |
33 |
34 | %%CLASSNAME
35 |
36 |
37 | AttachedMessage
38 |
39 |
40 | ^IOP.PrivateS9756.StopD
41 | StopDefaultData
42 | ^IOP.PrivateS9756.StopD
43 | ^IOP.PrivateS9756.StopI
44 | ^IOP.PrivateS9756.StopS
45 | %Storage.Persistent
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/src/iop/cls/IOP/Test.cls:
--------------------------------------------------------------------------------
1 | /// Description
2 | Class IOP.Test Extends %Persistent
3 | {
4 |
5 | /// Description
6 | ClassMethod TestJsonStringMessage() As %Status
7 | {
8 | set msg = ##class(IOP.Message).%New()
9 | set msg.classname = "IOP.Message"
10 | set msg.json = "{""name"":""John""}"
11 | set tset = msg.json
12 | set tst = msg.jstr
13 | QUIT $$$OK
14 | }
15 |
16 | ClassMethod TestJsonStreamMessage() As %Status
17 | {
18 | set msg = ##class(IOP.Message).%New()
19 | set msg.classname = "IOP.Message"
20 | set stream = ##class(%Stream.GlobalCharacter).%New()
21 | set sc = stream.Write("{""name"":""John""}")
22 | set msg.json = stream
23 | set tset = msg.json
24 | set tst = msg.jstr
25 | QUIT $$$OK
26 | }
27 |
28 | /// Register
29 | ClassMethod Register() As %Status
30 | {
31 | Set sc = $$$OK
32 | zw ##class(IOP.Utils).RegisterComponent("MyBusinessOperationWithAdapter","MyBusinessOperationWithAdapter","/irisdev/app/src/python/demo/",1,"PEX.MyBusinessOperationWithAdapter")
33 | #dim array as %ArrayOfObjects
34 | Return sc
35 | }
36 |
37 | ClassMethod TestBO() As %Status
38 | {
39 | try {
40 | Set sc = $$$OK
41 | set mybo = ##class(IOP.BusinessOperation).%New("mybo")
42 | set mybo.%classpaths = "/irisdev/app/src/python/demo"
43 | set mybo.%module = "MyBusinessOperationWithAdapter"
44 | set mybo.%classname = "MyBusinessOperationWithAdapter"
45 | $$$ThrowOnError(mybo.OnInit())
46 | set request = ##class(Ens.StringRequest).%New("hello")
47 | set request = ##class(EnsLib.PEX.Message).%New()
48 | set request.%classname = "MyRequest.MyRequest"
49 | set dyna = {"requestString":"hello!"}
50 | set request.%jsonObject = dyna
51 | Try {
52 | $$$ThrowOnError(mybo.OnMessage(request,.response))
53 | zw response
54 | } catch importEx {
55 | WRITE $System.Status.GetOneStatusText(importEx.AsStatus(),1),!
56 | }
57 | } catch ex {
58 | WRITE $System.Status.GetOneStatusText(ex.AsStatus(),1),!
59 | }
60 |
61 | Return sc
62 | }
63 |
64 | /// List
65 | ClassMethod PythonList() [ Language = python ]
66 | {
67 | return [ 1, 3 ]
68 | }
69 |
70 | Storage Default
71 | {
72 |
73 |
74 | %%CLASSNAME
75 |
76 |
77 | ^IOP.TestD
78 | TestDefaultData
79 | ^IOP.TestD
80 | ^IOP.TestI
81 | ^IOP.TestS
82 | %Storage.Persistent
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/src/iop/wsgi/handlers.py:
--------------------------------------------------------------------------------
1 | import os, sys, importlib, urllib.parse
2 | from io import BytesIO
3 |
4 | __ospath = os.getcwd()
5 |
6 | import iris
7 |
8 | os.chdir(__ospath)
9 |
10 | enc, esc = sys.getfilesystemencoding(), 'surrogateescape'
11 |
12 | rest_service = iris.cls('%REST.Impl')
13 |
14 | def get_from_module(app_path, app_module, app_name):
15 |
16 | # Add the path to the application
17 | if (app_path not in sys.path) :
18 | sys.path.append(app_path)
19 |
20 | # retrieve the application
21 | return getattr(importlib.import_module(app_module), app_name)
22 |
23 | # Changes the current working directory to the manager directory of the instance.
24 | def goto_manager_dir():
25 | iris.system.Process.CurrentDirectory(iris.system.Util.ManagerDirectory())
26 |
27 | def unicode_to_wsgi(u):
28 | # Convert an environment variable to a WSGI "bytes-as-unicode" string
29 | return u.encode(enc, esc).decode('iso-8859-1')
30 |
31 | def wsgi_to_bytes(s):
32 | return s.encode('iso-8859-1')
33 |
34 | def write(chunk):
35 | rest_service._WriteResponse(chunk)
36 |
37 | def start_response(status, response_headers, exc_info=None):
38 | '''WSGI start_response callable'''
39 | if exc_info:
40 | try:
41 | raise exc_info[1].with_traceback(exc_info[2])
42 | finally:
43 | exc_info = None
44 |
45 | rest_service._SetStatusCode(status)
46 | for tuple in response_headers:
47 | rest_service._SetHeader(tuple[0], tuple[1])
48 | return write
49 |
50 |
51 | # Make request to application
52 | def make_request(environ, stream, application, path):
53 |
54 | # Change the working directory for logging purposes
55 | goto_manager_dir()
56 |
57 | error_log_file = open('WSGI.log', 'a+')
58 |
59 | # We want the working directory to be the app's directory
60 | if (not path.endswith(os.path.sep)):
61 | path = path + os.path.sep
62 |
63 | #iris.system.Process.CurrentDirectory(path)
64 |
65 | # Set up the body of the request
66 | if stream != '':
67 | bytestream = stream
68 | elif (environ['CONTENT_TYPE'] == 'application/x-www-form-urlencoded'):
69 | bytestream = BytesIO()
70 | part = urllib.parse.urlencode(environ['formdata'])
71 | bytestream.write(part.encode('utf-8'))
72 | bytestream.seek(0)
73 | else:
74 | bytestream = BytesIO(b'')
75 |
76 | #for k,v in os.environ.items():
77 | #environ[k] = unicode_to_wsgi(v)
78 | environ['wsgi.input'] = bytestream
79 | environ['wsgi.errors'] = error_log_file
80 | environ['wsgi.version'] = (1, 0)
81 | environ['wsgi.multithread'] = False
82 | environ['wsgi.multiprocess'] = True
83 | environ['wsgi.run_once'] = True
84 |
85 |
86 | if environ.get('HTTPS', 'off') in ('on', '1'):
87 | environ['wsgi.url_scheme'] = 'https'
88 | else:
89 | environ['wsgi.url_scheme'] = 'http'
90 |
91 | # Calling WSGI application
92 | response = application(environ, start_response)
93 |
94 | error_log_file.close()
95 |
96 | try:
97 | for data in response:
98 | if data:
99 | # (REST.Impl).Write() needs a utf-8 string
100 | write(data.decode('utf-8'))
101 | write(b'')
102 | finally:
103 | if hasattr(response, 'close'):
104 | response.close()
105 |
--------------------------------------------------------------------------------
/src/tests/bench/bench_bo.py:
--------------------------------------------------------------------------------
1 | from iop import BusinessOperation
2 | import time
3 |
4 | class BenchIoPOperation(BusinessOperation):
5 |
6 | my_param = "BenchIoPOperation"
7 |
8 | def on_message(self, request):
9 | time.sleep(0.01) # Simulate some processing delay
10 | return request
11 |
--------------------------------------------------------------------------------
/src/tests/bench/bench_bp.py:
--------------------------------------------------------------------------------
1 | from iop import BusinessProcess
2 |
3 | class BenchIoPProcess(BusinessProcess):
4 | def on_init(self):
5 | if not hasattr(self, 'size'):
6 | self.size = 100
7 | if not hasattr(self, 'target'):
8 | self.target = 'Python.BenchIoPOperation'
9 |
10 | def on_message(self, request):
11 | for _ in range(self.size):
12 | _ = self.send_request_sync(self.target,request)
--------------------------------------------------------------------------------
/src/tests/bench/cls/Bench.Operation.cls:
--------------------------------------------------------------------------------
1 | Class Bench.Operation Extends Ens.BusinessOperation
2 | {
3 |
4 | Parameter INVOCATION = "Queue";
5 |
6 | Method Method(
7 | pRequest As Ens.Request,
8 | Output pResponse As Ens.Response) As %Status
9 | {
10 | set tStatus = $$$OK
11 | set pResponse = ##class(Ens.Response).%New()
12 |
13 | try{
14 | // Simulate some processing time
15 | hang 0.01
16 | set pResponse = pRequest
17 |
18 | }
19 | catch exp
20 | {
21 | set tStatus = exp.AsStatus()
22 | }
23 | Quit tStatus
24 | }
25 |
26 | XData MessageMap
27 | {
28 |
29 |
30 | Method
31 |
32 |
33 | Method
34 |
35 |
36 | Method
37 |
38 |
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/src/tests/bench/cls/Bench.Process.cls:
--------------------------------------------------------------------------------
1 | Class Bench.Process Extends Ens.BusinessProcess
2 | {
3 |
4 | Property TargetConfigName As %String(MAXLEN = 1000) [ InitialExpression = "Bench.Operation" ];
5 |
6 | Property Size As %Integer [ InitialExpression = 100 ];
7 |
8 | Parameter SETTINGS = "Size:Basic,TargetConfigName:Basic";
9 |
10 | Method OnRequest(
11 | pDocIn As %Library.Persistent,
12 | Output pDocOut As %Library.Persistent) As %Status
13 | {
14 | set status = $$$OK
15 |
16 | try {
17 |
18 | for i=1:1:..Size {
19 | $$$ThrowOnError(..SendRequestSync(..TargetConfigName,pDocIn,.pDocOut))
20 | }
21 |
22 | } catch ex {
23 | set status = ex.AsStatus()
24 | }
25 |
26 | Quit status
27 | }
28 |
29 | Storage Default
30 | {
31 |
32 | "Process"
33 |
34 | TargetConfigNames
35 |
36 |
37 | Size
38 |
39 |
40 | TargetConfigName
41 |
42 |
43 | ProcessDefaultData
44 | %Storage.Persistent
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/src/tests/bench/msg.py:
--------------------------------------------------------------------------------
1 | from iop import PydanticMessage
2 | from iop import Message
3 | from iop import PickleMessage
4 | from iop import PydanticPickleMessage
5 | from dataclasses import dataclass
6 |
7 | @dataclass
8 | class MyMessage(Message):
9 | message : str = None
10 |
11 | class MyPydanticMessage(PydanticMessage):
12 | message : str = None
13 |
14 | @dataclass
15 | class MyPickleMessage(PickleMessage):
16 | message : str = None
17 |
18 | class MyPydanticPickleMessage(PydanticPickleMessage):
19 | message : str = None
--------------------------------------------------------------------------------
/src/tests/bench/settings.py:
--------------------------------------------------------------------------------
1 | from bench_bo import BenchIoPOperation
2 | from bench_bp import BenchIoPProcess
3 |
4 | import os
5 |
6 | import iris
7 |
8 | # get current directory
9 | current_dir = os.path.dirname(os.path.realpath(__file__))
10 | # get working directory
11 | working_dir = os.path.abspath(os.path.join(current_dir, os.pardir))
12 | # get the absolute path of 'src'
13 | src_dir = os.path.abspath(os.path.join(working_dir, os.pardir))
14 |
15 | # create a strings with current_dir and src_dir with a | separator
16 | classpaths = f"{current_dir}|{src_dir}"
17 |
18 | # load Cos Classes
19 | iris.cls('%SYSTEM.OBJ').LoadDir(os.path.join(
20 | current_dir, 'cls'), 'cubk', "*.cls", 1)
21 |
22 | CLASSES = {
23 | "Python.BenchIoPOperation": BenchIoPOperation,
24 | "Python.BenchIoPProcess": BenchIoPProcess,
25 | }
26 |
27 | PRODUCTIONS = [{
28 | "Bench.Production": {
29 | "@Name": "Bench.Production",
30 | "@TestingEnabled": "true",
31 | "@LogGeneralTraceEvents": "false",
32 | "Description": "",
33 | "ActorPoolSize": "1",
34 | "Item": [
35 | {
36 | "@Name": "Bench.Operation",
37 | "@Category": "",
38 | "@ClassName": "Bench.Operation",
39 | },
40 | {
41 | "@Name": "Python.BenchIoPOperation",
42 | "@Category": "",
43 | "@ClassName": "Python.BenchIoPOperation",
44 | "@PoolSize": "1",
45 | "@Enabled": "true",
46 | "@Foreground": "false",
47 | "@Comment": "",
48 | "@LogTraceEvents": "false",
49 | "@Schedule": "",
50 | "Setting": {
51 | "@Target": "Host",
52 | "@Name": "%classpaths",
53 | "#text": classpaths
54 | }
55 | },
56 | {
57 | "@Name": "Python.BenchIoPProcess",
58 | "@Category": "",
59 | "@ClassName": "Python.BenchIoPProcess",
60 | "@PoolSize": "0",
61 | "@Enabled": "true",
62 | "@Foreground": "false",
63 | "@Comment": "",
64 | "@LogTraceEvents": "false",
65 | "@Schedule": "",
66 | "Setting": {
67 | "@Target": "Host",
68 | "@Name": "%classpaths",
69 | "#text": classpaths
70 | }
71 | },
72 | {
73 | "@Name": "Python.BenchIoPProcess.To.Cls",
74 | "@Category": "",
75 | "@ClassName": "Python.BenchIoPProcess",
76 | "@PoolSize": "0",
77 | "@Enabled": "true",
78 | "@Foreground": "false",
79 | "@Comment": "",
80 | "@LogTraceEvents": "false",
81 | "@Schedule": "",
82 | "Setting": [{
83 | "@Target": "Host",
84 | "@Name": "%classpaths",
85 | "#text": classpaths
86 | }, {
87 | "@Target": "Host",
88 | "@Name": "%settings",
89 | "#text": "target=Bench.Operation"
90 | }]
91 | },
92 | {
93 | "@Name": "Bench.Process",
94 | "@Category": "",
95 | "@ClassName": "Bench.Process",
96 | "@PoolSize": "1",
97 | "@Enabled": "true",
98 | "@Foreground": "false",
99 | "@Comment": "",
100 | "@LogTraceEvents": "false",
101 | "@Schedule": "",
102 | "Setting": {
103 | "@Target": "Host",
104 | "@Name": "TargetConfigName",
105 | "#text": "Python.BenchIoPOperation"
106 | }
107 | },
108 | {
109 | "@Name": "Bench.Process.To.Cls",
110 | "@Category": "",
111 | "@ClassName": "Bench.Process",
112 | "@PoolSize": "1",
113 | "@Enabled": "true",
114 | "@Foreground": "false",
115 | "@Comment": "",
116 | "@LogTraceEvents": "false",
117 | "@Schedule": "",
118 | "Setting": {
119 | "@Target": "Host",
120 | "@Name": "TargetConfigName",
121 | "#text": "Bench.Operation"
122 | }
123 | }
124 | ]
125 | }
126 | }
127 | ]
128 |
--------------------------------------------------------------------------------
/src/tests/cls/ComplexGet.cls:
--------------------------------------------------------------------------------
1 | Class UnitTest.ComplexGet Extends Ens.DataTransformDTL [ DependsOn = (IOP.Message, Ens.StringRequest) ]
2 | {
3 |
4 | Parameter GENERATEEMPTYSEGMENTS = 0;
5 |
6 | Parameter IGNOREMISSINGSOURCE = 1;
7 |
8 | Parameter REPORTERRORS = 1;
9 |
10 | Parameter TREATEMPTYREPEATINGFIELDASNULL = 0;
11 |
12 | XData DTL [ XMLNamespace = "http://www.intersystems.com/dtl" ]
13 | {
14 |
15 |
16 |
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/tests/cls/ComplexGetList.cls:
--------------------------------------------------------------------------------
1 | Class UnitTest.ComplexGetList Extends Ens.DataTransformDTL [ DependsOn = (IOP.Message, Ens.StringRequest) ]
2 | {
3 |
4 | Parameter GENERATEEMPTYSEGMENTS = 0;
5 |
6 | Parameter IGNOREMISSINGSOURCE = 1;
7 |
8 | Parameter REPORTERRORS = 1;
9 |
10 | Parameter TREATEMPTYREPEATINGFIELDASNULL = 0;
11 |
12 | XData DTL [ XMLNamespace = "http://www.intersystems.com/dtl" ]
13 | {
14 |
15 |
16 |
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/tests/cls/ComplexTransform.cls:
--------------------------------------------------------------------------------
1 | Class UnitTest.ComplexTransform Extends Ens.DataTransformDTL [ DependsOn = IOP.Message ]
2 | {
3 |
4 | Parameter IGNOREMISSINGSOURCE = 1;
5 |
6 | Parameter REPORTERRORS = 1;
7 |
8 | Parameter TREATEMPTYREPEATINGFIELDASNULL = 0;
9 |
10 | XData DTL [ XMLNamespace = "http://www.intersystems.com/dtl" ]
11 | {
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/tests/cls/SimpleMessageGet.cls:
--------------------------------------------------------------------------------
1 | Class UnitTest.SimpleMessageGet Extends Ens.DataTransformDTL [ DependsOn = (IOP.Message, Ens.StringResponse) ]
2 | {
3 |
4 | Parameter GENERATEEMPTYSEGMENTS = 0;
5 |
6 | Parameter IGNOREMISSINGSOURCE = 1;
7 |
8 | Parameter REPORTERRORS = 1;
9 |
10 | Parameter TREATEMPTYREPEATINGFIELDASNULL = 0;
11 |
12 | XData DTL [ XMLNamespace = "http://www.intersystems.com/dtl" ]
13 | {
14 |
15 |
16 |
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/tests/cls/SimpleMessageSet.cls:
--------------------------------------------------------------------------------
1 | Class UnitTest.SimpleMessageSet Extends Ens.DataTransformDTL [ DependsOn = (IOP.Message, IOP.Message) ]
2 | {
3 |
4 | Parameter GENERATEEMPTYSEGMENTS = 0;
5 |
6 | Parameter IGNOREMISSINGSOURCE = 1;
7 |
8 | Parameter REPORTERRORS = 1;
9 |
10 | Parameter TREATEMPTYREPEATINGFIELDASNULL = 0;
11 |
12 | XData DTL [ XMLNamespace = "http://www.intersystems.com/dtl" ]
13 | {
14 |
15 |
16 |
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/tests/cls/SimpleMessageSetVDoc.cls:
--------------------------------------------------------------------------------
1 | Class UnitTest.SimpleMessageSetVDoc Extends Ens.DataTransformDTL [ DependsOn = (IOP.Message, IOP.Message) ]
2 | {
3 |
4 | Parameter GENERATEEMPTYSEGMENTS = 0;
5 |
6 | Parameter IGNOREMISSINGSOURCE = 1;
7 |
8 | Parameter REPORTERRORS = 1;
9 |
10 | Parameter TREATEMPTYREPEATINGFIELDASNULL = 0;
11 |
12 | XData DTL [ XMLNamespace = "http://www.intersystems.com/dtl" ]
13 | {
14 |
15 |
16 |
17 |
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from os.path import dirname as d
3 | from os.path import abspath, join
4 | root_dir = d(d(abspath(__file__)))
5 | sys.path.append(root_dir)
6 | # add registerFiles to the path
7 | sys.path.append(join(join(root_dir, 'tests'), 'registerFilesIop'))
8 |
9 | # from iop import Utils
10 |
11 | # Utils.setup()
--------------------------------------------------------------------------------
/src/tests/registerFilesIop/adapter.py:
--------------------------------------------------------------------------------
1 | import iop
2 | import requests
3 | import iris
4 | import json
5 |
6 | class RedditInboundAdapter(iop.InboundAdapter):
7 |
8 | def OnInit(self):
9 |
10 | if not hasattr(self,'Feed'):
11 | self.Feed = "/new/"
12 |
13 | if self.Limit is None:
14 | raise TypeError('no Limit field')
15 |
16 | self.LastPostName = ""
17 |
18 | return 1
19 |
20 | def OnTask(self):
21 | self.LOGINFO(f"LIMIT:{self.Limit}")
22 | if self.Feed == "" :
23 | return 1
24 |
25 | tSC = 1
26 | # HTTP Request
27 | try:
28 | server = "https://www.reddit.com"
29 | requestString = self.Feed+".json?before="+self.LastPostName+"&limit="+self.Limit
30 | self.LOGINFO(server+requestString)
31 | response = requests.get(server+requestString)
32 | response.raise_for_status()
33 |
34 | data = response.json()
35 | updateLast = 0
36 |
37 | for key, value in enumerate(data['data']['children']):
38 | if value['data']['selftext']=="":
39 | continue
40 | post = iris.cls('dc.Reddit.Post')._New()
41 | post._JSONImport(json.dumps(value['data']))
42 | post.OriginalJSON = json.dumps(value)
43 | if not updateLast:
44 | self.LastPostName = value['data']['name']
45 | updateLast = 1
46 | response = self.BusinessHost.ProcessInput(post)
47 | except requests.exceptions.HTTPError as err:
48 | if err.response.status_code == 429:
49 | self.LOGWARNING(err.__str__())
50 | else:
51 | raise err
52 | except Exception as err:
53 | self.LOGERROR(err.__str__())
54 | raise err
55 |
56 | return tSC
--------------------------------------------------------------------------------
/src/tests/registerFilesIop/bo.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | from iop import BusinessOperation
4 |
5 | import iris
6 |
7 | from message import MyResponse
8 |
9 | import os
10 | import datetime
11 | import smtplib
12 | from email.mime.text import MIMEText
13 |
14 | class EmailOperation(BusinessOperation):
15 |
16 | def OnMessage(self, pRequest):
17 |
18 | sender = 'admin@example.com'
19 | receivers = [ pRequest.ToEmailAddress ]
20 |
21 |
22 | port = 1025
23 | msg = MIMEText('This is test mail')
24 |
25 | msg['Subject'] = pRequest.Found+" found"
26 | msg['From'] = 'admin@example.com'
27 | msg['To'] = pRequest.ToEmailAddress
28 |
29 | with smtplib.SMTP('localhost', port) as server:
30 |
31 | # server.login('username', 'password')
32 | server.sendmail(sender, receivers, msg.as_string())
33 | print("Successfully sent email")
34 |
35 |
36 |
37 | class EmailOperationWithIrisAdapter(BusinessOperation):
38 |
39 | def get_adapter_type():
40 | """
41 | Name of the registred Adapter
42 | """
43 | return "EnsLib.EMail.OutboundAdapter"
44 |
45 | def OnMessage(self, pRequest):
46 |
47 | mailMessage = iris.cls("%Net.MailMessage")._New()
48 | mailMessage.Subject = pRequest.Found+" found"
49 | self.Adapter.AddRecipients(mailMessage,pRequest.ToEmailAddress)
50 | mailMessage.Charset="UTF-8"
51 |
52 | title = author = url = ""
53 | if (pRequest.Post is not None) :
54 | title = pRequest.Post.title
55 | author = pRequest.Post.author
56 | url = pRequest.Post.url
57 |
58 | mailMessage.TextData.WriteLine("More info:")
59 | mailMessage.TextData.WriteLine("Title: "+title)
60 | mailMessage.TextData.WriteLine("Author: "+author)
61 | mailMessage.TextData.WriteLine("URL: "+url)
62 |
63 | return self.Adapter.SendMail(mailMessage)
64 |
65 | class FileOperation(BusinessOperation):
66 |
67 | def OnInit(self):
68 | if hasattr(self,'Path'):
69 | os.chdir(self.Path)
70 |
71 | def OnMessage(self, pRequest):
72 |
73 | ts = title = author = url = text = ""
74 |
75 | if (pRequest.Post is not None):
76 | title = pRequest.Post.Title
77 | author = pRequest.Post.Author
78 | url = pRequest.Post.Url
79 | text = pRequest.Post.Selftext
80 | ts = datetime.datetime.fromtimestamp(pRequest.Post.CreatedUTC).__str__()
81 |
82 | line = ts+" : "+title+" : "+author+" : "+url
83 | filename = pRequest.Found+".txt"
84 |
85 |
86 | self.PutLine(filename, line)
87 | self.PutLine(filename, "")
88 | self.PutLine(filename, text)
89 | self.PutLine(filename, " * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *")
90 |
91 | return
92 |
93 | def PutLine(self,filename,string):
94 | try:
95 | with open(filename, "a",encoding="utf-8") as outfile:
96 | outfile.write(string)
97 | except Exception as e:
98 | raise e
99 |
100 | class FileOperationWithIrisAdapter(BusinessOperation):
101 |
102 | def get_adapter_type():
103 | """
104 | Name of the registred Adapter
105 | """
106 | return "EnsLib.File.OutboundAdapter"
107 |
108 | def OnMessage(self, pRequest):
109 |
110 | ts = title = author = url = text = ""
111 |
112 | if (pRequest.Post != ""):
113 | title = pRequest.Post.Title
114 | author = pRequest.Post.Author
115 | url = pRequest.Post.Url
116 | text = pRequest.Post.Selftext
117 | ts = iris.cls("%Library.PosixTime").LogicalToOdbc(iris.cls("%Library.PosixTime").UnixTimeToLogical(pRequest.Post.CreatedUTC))
118 |
119 | line = ts+" : "+title+" : "+author+" : "+url
120 | filename = pRequest.Found+".txt"
121 |
122 | self.Adapter.PutLine(filename, line)
123 | self.Adapter.PutLine(filename, "")
124 | self.Adapter.PutLine(filename, text)
125 | self.Adapter.PutLine(filename, " * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *")
126 |
127 | return
128 |
129 | class MyOperation(BusinessOperation):
130 |
131 | def OnMessage(self, request):
132 | self.log_info("Received message: "+str(request))
133 |
134 | class MySettingOperation(BusinessOperation):
135 |
136 | my_empty_var : str
137 | my_none_var = None
138 | my_int_var :int = 0
139 | my_float_var : float = 0.0
140 | my_untyped_var = 0
141 | my_str_var = "foo"
142 | my_very_long_var = "a" * 1000 # Long string for testing
143 |
144 | def OnMessage(self, request):
145 | attr = request.StringValue
146 | return MyResponse(self.__getattribute__(attr))
147 |
148 | if __name__ == "__main__":
149 |
150 | op = FileOperation()
151 | from message import PostMessage,PostClass
152 | msg = PostMessage(PostClass('foo','foo','foo','foo',1,'foo'),'bar','bar')
153 | op.OnMessage(msg)
--------------------------------------------------------------------------------
/src/tests/registerFilesIop/bp.py:
--------------------------------------------------------------------------------
1 | from iop import BusinessProcess
2 |
3 | from message import PostMessage
4 |
5 | class FilterPostRoutingRule(BusinessProcess):
6 |
7 | def OnInit(self):
8 |
9 | if not hasattr(self,'Target'):
10 | self.Target = "Python.FileOperation"
11 |
12 | return
13 |
14 | def OnRequest(self, request: PostMessage):
15 | if 'dog'.upper() in request.Post.Selftext.upper():
16 | request.ToEmailAddress = 'dog@company.com'
17 | request.Found = 'Dog'
18 | if 'cat'.upper() in request.Post.Selftext.upper():
19 | request.ToEmailAddress = 'cat@company.com'
20 | request.Found = 'Cat'
21 | return self.SendRequestSync(self.Target,request)
22 |
--------------------------------------------------------------------------------
/src/tests/registerFilesIop/edge/bs-dash.py:
--------------------------------------------------------------------------------
1 | from iop import BusinessService
2 |
3 | class BS(BusinessService):
4 |
5 | @staticmethod
6 | def get_adapter_type():
7 | return 'EnsLib.File.InboundAdapter'
--------------------------------------------------------------------------------
/src/tests/registerFilesIop/edge/bs_underscore.py:
--------------------------------------------------------------------------------
1 | from iop import BusinessService
2 |
3 | class BS(BusinessService):
4 |
5 | @staticmethod
6 | def get_adapter_type():
7 | return 'EnsLib.File.InboundAdapter'
--------------------------------------------------------------------------------
/src/tests/registerFilesIop/message.py:
--------------------------------------------------------------------------------
1 | from typing import List, Dict
2 | from iop import Message, PickleMessage, PydanticMessage
3 |
4 | from dataclasses import dataclass
5 |
6 | from obj import PostClass
7 |
8 | from datetime import datetime, date, time
9 |
10 | class PydanticPostClass(PydanticMessage):
11 | Title: str
12 | Selftext : str
13 | Author: str
14 | Url: str
15 | CreatedUTC: float = None
16 | OriginalJSON: str = None
17 |
18 | class PydanticSimpleMessage(PydanticMessage):
19 | integer:int
20 | string:str
21 |
22 | class PydanticFullMessage(PydanticMessage):
23 | embedded:PydanticPostClass
24 | embedded_list:List[PydanticPostClass]
25 | embedded_dict:Dict[str,PydanticPostClass]
26 | embedded_dataclass:PostClass
27 | string:str
28 | integer:int
29 | float:float
30 | boolean:bool
31 | list:List
32 | dikt:Dict
33 | list_dict:List[Dict]
34 | dict_list:Dict[str,List]
35 | date:date
36 | datetime:datetime
37 | time:time
38 |
39 | @dataclass
40 | class FullMessage(Message):
41 |
42 | embedded:PostClass
43 | embedded_list:List[PostClass]
44 | embedded_dict:Dict[str,PostClass]
45 | string:str
46 | integer:int
47 | float:float
48 | boolean:bool
49 | list:List
50 | dict:Dict
51 | list_dict:List[Dict]
52 | dict_list:Dict[str,List]
53 | date:date
54 | datetime:datetime
55 | time:time
56 |
57 | @dataclass
58 | class PostMessage(Message):
59 | Post:PostClass = None
60 | ToEmailAddress:str = None
61 | Found:str = None
62 |
63 | @dataclass
64 | class ComplexMessage(Message):
65 | post:PostClass = None
66 | string:str = None
67 | list_str:List[str] = None
68 | list_int:List[int] = None
69 | list_post:List[PostClass] = None
70 | dict_str:Dict[str,str] = None
71 | dict_int:Dict[str,int] = None
72 | dict_post:Dict[str,PostClass] = None
73 |
74 | @dataclass
75 | class MyResponse(Message):
76 | value:str = None
77 |
78 | @dataclass
79 | class SimpleMessage(Message):
80 | integer : int
81 | string : str
82 |
83 | class SimpleMessageNotDataclass(Message):
84 | integer : int
85 | string : str
86 |
87 | class SimpleMessageNotMessage:
88 | integer : int
89 | string : str
90 |
91 | @dataclass
92 | class PickledMessage(PickleMessage):
93 | integer : int
94 | string : str
--------------------------------------------------------------------------------
/src/tests/registerFilesIop/obj.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 |
3 | @dataclass
4 | class PostClass:
5 | Title: str
6 | Selftext : str
7 | Author: str
8 | Url: str
9 | CreatedUTC: float = None
10 | OriginalJSON: str = None
--------------------------------------------------------------------------------
/src/tests/registerFilesIop/settings.py:
--------------------------------------------------------------------------------
1 | import bp
2 | from bo import *
3 | from bs import RedditService
4 | from message import SimpleMessage, PostMessage
5 |
6 | SCHEMAS = [SimpleMessage, PostMessage]
7 |
8 | CLASSES = {
9 | 'Python.RedditService': RedditService,
10 | 'Python.FilterPostRoutingRule': bp.FilterPostRoutingRule,
11 | 'Python.bp': bp,
12 | 'Python.FileOperation': FileOperation,
13 | 'UnitTest.MySettingOperation': MySettingOperation,
14 | }
15 |
16 | PRODUCTIONS = [
17 | {
18 | "dc.Python.Production": {
19 | "@Name": "dc.Python.Production",
20 | "@TestingEnabled": "true",
21 | "@LogGeneralTraceEvents": "false",
22 | "Description": "",
23 | "ActorPoolSize": "2"
24 | }
25 | },
26 | {
27 | "Python.TestSettingProduction": {
28 | "@Name": "Python.TestSettingProduction",
29 | "@TestingEnabled": "true",
30 | "@LogGeneralTraceEvents": "false",
31 | "Description": "",
32 | "ActorPoolSize": "2",
33 | "Item": [
34 | {
35 | "@Name": "UnitTest.MySettingOperation",
36 | "@Enabled": "true",
37 | "@ClassName": "UnitTest.MySettingOperation",
38 | "Setting": [
39 | {
40 | "@Target": "Host",
41 | "@Name": "my_int_var",
42 | "#text": "1"
43 | },
44 | {
45 | "@Target": "Host",
46 | "@Name": "my_float_var",
47 | "#text": "1.0"
48 | },
49 | {
50 | "@Target": "Host",
51 | "@Name": "my_untyped_var",
52 | "#text": "1"
53 | },
54 | {
55 | "@Target": "Host",
56 | "@Name": "my_str_var",
57 | "#text": "bar"
58 | }
59 | ]
60 | }
61 | ]
62 | }
63 | }
64 | ]
--------------------------------------------------------------------------------
/src/tests/test_adapters.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from unittest.mock import MagicMock, patch
3 |
4 | from iop._inbound_adapter import _InboundAdapter
5 | from iop._outbound_adapter import _OutboundAdapter
6 |
7 | @pytest.fixture
8 | def inbound_adapter():
9 | adapter = _InboundAdapter()
10 | adapter.iris_handle = MagicMock()
11 | return adapter
12 |
13 | @pytest.fixture
14 | def outbound_adapter():
15 | adapter = _OutboundAdapter()
16 | adapter.iris_handle = MagicMock()
17 | return adapter
18 |
19 | class TestInboundAdapter:
20 | def test_set_iris_handles(self, inbound_adapter):
21 | handle_current = MagicMock()
22 | handle_partner = MagicMock()
23 | handle_partner.GetClass = MagicMock(return_value="TestClass")
24 |
25 | inbound_adapter._set_iris_handles(handle_current, handle_partner)
26 |
27 | assert inbound_adapter.iris_handle == handle_current
28 | assert inbound_adapter.BusinessHost == handle_partner
29 | assert inbound_adapter.business_host == handle_partner
30 | assert inbound_adapter.business_host_python == "TestClass"
31 |
32 | def test_on_task_calls_deprecated(self, inbound_adapter):
33 | # Test that on_task calls OnTask for backwards compatibility
34 | with patch.object(inbound_adapter, 'OnTask', return_value="test_response") as mock_on_task:
35 | result = inbound_adapter.on_task()
36 |
37 | mock_on_task.assert_called_once()
38 | assert result == "test_response"
39 |
40 | class TestOutboundAdapter:
41 | def test_set_iris_handles(self, outbound_adapter):
42 | handle_current = MagicMock()
43 | handle_partner = MagicMock()
44 | handle_partner.GetClass = MagicMock(return_value="TestClass")
45 |
46 | outbound_adapter._set_iris_handles(handle_current, handle_partner)
47 |
48 | assert outbound_adapter.iris_handle == handle_current
49 | assert outbound_adapter.BusinessHost == handle_partner
50 | assert outbound_adapter.business_host == handle_partner
51 | assert outbound_adapter.business_host_python == "TestClass"
52 |
53 | def test_on_keepalive(self, outbound_adapter):
54 | assert outbound_adapter.on_keepalive() is None
55 |
--------------------------------------------------------------------------------
/src/tests/test_bproduction_settings.py:
--------------------------------------------------------------------------------
1 | from iop._director import _Director
2 | from iop._utils import _Utils
3 |
4 | import os
5 |
6 | class TestProductionSettings:
7 | @classmethod
8 | def setup_class(cls):
9 | path = 'registerFilesIop/settings.py'
10 | # get path of the current fille script
11 | path = os.path.join(os.path.dirname(__file__), path)
12 | _Utils.migrate(path)
13 | _Director.stop_production()
14 | _Director.set_default_production('Python.TestSettingProduction')
15 | _Director.start_production()
16 |
17 | def test_my_none_var(self):
18 | rsp = _Director.test_component('UnitTest.MySettingOperation',None,'iris.Ens.StringRequest',"my_none_var")
19 | assert rsp.value == None
20 |
21 | def test_my_str_var(self):
22 | rsp = _Director.test_component('UnitTest.MySettingOperation',None,'iris.Ens.StringRequest',"my_str_var")
23 | assert rsp.value == "bar"
24 |
25 |
26 | @classmethod
27 | def teardown_class(cls):
28 | _Director.stop_production()
29 | _Director.set_default_production('test')
30 |
--------------------------------------------------------------------------------
/src/tests/test_business_operation.py:
--------------------------------------------------------------------------------
1 | import iris
2 | import pytest
3 | from unittest.mock import MagicMock, patch
4 | from iop._business_operation import _BusinessOperation
5 | from iop._dispatch import dispach_message, dispatch_serializer
6 | from registerFilesIop.message import SimpleMessage
7 |
8 | @pytest.fixture
9 | def operation():
10 | op = _BusinessOperation()
11 | op.iris_handle = MagicMock()
12 | return op
13 |
14 | def test_message_handling(operation):
15 | # Test on_message
16 | request = SimpleMessage(integer=1, string='test')
17 | assert operation.on_message(request) is None
18 |
19 | # Test deprecated OnMessage
20 | assert operation.OnMessage(request) is None
21 |
22 | def test_keepalive(operation):
23 | assert operation.on_keepalive() is None
24 |
25 | def test_adapter_handling():
26 | # Test adapter setup with mock IRIS adapter
27 | op = _BusinessOperation()
28 | mock_current = MagicMock()
29 | mock_partner = MagicMock()
30 |
31 | # Setup mock IRIS adapter
32 | mock_partner._IsA.return_value = True
33 | mock_partner.GetModule.return_value = "some.module"
34 | mock_partner.GetClassname.return_value = "SomeAdapter"
35 |
36 | with patch('importlib.import_module') as mock_import:
37 | mock_module = MagicMock()
38 | mock_import.return_value = mock_module
39 | op._set_iris_handles(mock_current, mock_partner)
40 |
41 | assert op.iris_handle == mock_current
42 |
43 | def test_dispatch_methods(operation):
44 | # Test dispatch initialization
45 | operation.DISPATCH = [("MessageType1", "handle_type1")]
46 | mock_host = MagicMock()
47 | mock_host.port=0
48 | mock_host.enable=False
49 |
50 | operation._dispatch_on_init(mock_host)
51 |
52 | # Test message dispatch
53 | request = SimpleMessage(integer=1, string='test')
54 | operation._dispatch_on_message(request)
55 |
56 | # Verify internal method calls
57 | operation.iris_handle.dispatchOnMessage.assert_not_called()
58 |
59 | def test_dispatch_on_message(operation):
60 | class CustomOperation(_BusinessOperation):
61 | def handle_simple(self, request: SimpleMessage):
62 | return SimpleMessage(integer=request.integer + 1, string="handled")
63 | # Test dispatch with no handlers
64 | request = iris.cls("IOP.Message")._New()
65 | request.json = '{"integer": 1, "string": "test"}'
66 | request.classname = 'registerFilesIop.message.SimpleMessage'
67 | operation = CustomOperation()
68 | mock_host = MagicMock()
69 | mock_host.port=0
70 | mock_host.enable=False
71 | operation._dispatch_on_init(mock_host)
72 | response = operation._dispatch_on_message(request)
73 | excepted_response = dispatch_serializer(SimpleMessage(integer=2, string='handled'))
74 |
75 | assert response.json == excepted_response.json
76 |
77 | def test_dispatch_with_custom_handlers():
78 | class CustomOperation(_BusinessOperation):
79 | def handle_simple(self, request: SimpleMessage):
80 | return SimpleMessage(integer=request.integer + 1, string="handled")
81 |
82 | operation = CustomOperation()
83 | mock_host = MagicMock()
84 | mock_host.port=0
85 | mock_host.enable=False
86 | operation._dispatch_on_init(mock_host)
87 | operation.iris_handle = MagicMock()
88 |
89 | request = SimpleMessage(integer=1, string='test')
90 | response = dispach_message(operation,request)
91 |
92 | assert isinstance(response, SimpleMessage)
93 | assert response.integer == 2
94 | assert response.string == "handled"
95 |
--------------------------------------------------------------------------------
/src/tests/test_business_process.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from unittest.mock import MagicMock, patch
3 | from iop._business_process import _BusinessProcess
4 | from iop._dispatch import dispatch_serializer
5 | from registerFilesIop.message import SimpleMessage, PickledMessage, FullMessage
6 |
7 | @pytest.fixture
8 | def process():
9 | proc = _BusinessProcess()
10 | proc.iris_handle = MagicMock()
11 | return proc
12 |
13 | def test_message_handling(process):
14 | # Test on_message
15 | request = SimpleMessage(integer=1, string='test')
16 | assert process.on_message(request) is None
17 |
18 | # Test on_request
19 | assert process.on_request(request) is None
20 |
21 | # Test on_response
22 | response = SimpleMessage(integer=2, string='response')
23 | call_request = SimpleMessage(integer=3, string='call_request')
24 | call_response = SimpleMessage(integer=4, string='call_response')
25 | completion_key = "test_key"
26 |
27 | assert process.on_response(
28 | request, response, call_request, call_response, completion_key
29 | ) == response
30 |
31 | # Test on_complete
32 | assert process.on_complete(request, response) == response
33 |
34 | def test_async_operations(process):
35 | # Test send_request_async
36 | target = "target_service"
37 | request = SimpleMessage(integer=1, string='test')
38 | process.send_request_async(target, request)
39 | process.iris_handle.dispatchSendRequestAsync.assert_called_once()
40 |
41 | # Test set_timer
42 | timeout = 1000
43 | completion_key = "timer_key"
44 | process.set_timer(timeout, completion_key)
45 | process.iris_handle.dispatchSetTimer.assert_called_once_with(
46 | timeout, completion_key
47 | )
48 |
49 | def test_persistent_properties():
50 | # Test persistent property handling
51 | class ProcessWithProperties(_BusinessProcess):
52 | PERSISTENT_PROPERTY_LIST = ["test_prop"]
53 | def __init__(self):
54 | super().__init__()
55 | self.test_prop = "test_value"
56 |
57 | process = ProcessWithProperties()
58 | mock_host = MagicMock()
59 |
60 | # Test save properties
61 | process._save_persistent_properties(mock_host)
62 | mock_host.setPersistentProperty.assert_called_once_with("test_prop", "test_value")
63 |
64 | # Test restore properties
65 | mock_host.getPersistentProperty.return_value = "restored_value"
66 | process._restore_persistent_properties(mock_host)
67 | assert process.test_prop == "restored_value"
68 |
69 | def test_dispatch_methods(process):
70 | mock_host = MagicMock()
71 | mock_host.port=0
72 | mock_host.enable=False
73 |
74 | request = SimpleMessage(integer=1, string='test')
75 | response = SimpleMessage(integer=2, string='response')
76 |
77 | # Test dispatch methods
78 | process._dispatch_on_init(mock_host)
79 | process._dispatch_on_connected(mock_host)
80 | process._dispatch_on_request(mock_host, request)
81 | process._dispatch_on_response(
82 | mock_host, request, response, request, response, "completion_key"
83 | )
84 | process._dispatch_on_tear_down(mock_host)
85 |
86 | def test_reply(process):
87 | response = SimpleMessage(integer=1, string='test')
88 | process.reply(response)
89 | process.iris_handle.dispatchReply.assert_called_once()
90 |
--------------------------------------------------------------------------------
/src/tests/test_business_service.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from unittest.mock import MagicMock, patch
3 | from iop._business_service import _BusinessService
4 | from registerFilesIop.message import SimpleMessage
5 |
6 | @pytest.fixture
7 | def service():
8 | svc = _BusinessService()
9 | svc.iris_handle = MagicMock()
10 | return svc
11 |
12 | def test_process_input(service):
13 | # Test on_process_input
14 | message = SimpleMessage(integer=1, string='test')
15 | assert service.on_process_input(message) is None
16 |
17 | # Test deprecated OnProcessInput
18 | assert service.OnProcessInput(message) is None
19 |
20 | def test_adapter_handling():
21 | # Test adapter setup with mock IRIS adapter
22 | svc = _BusinessService()
23 | mock_current = MagicMock()
24 | mock_partner = MagicMock()
25 |
26 | # Setup mock IRIS adapter
27 | mock_partner._IsA.return_value = True
28 | mock_partner.GetModule.return_value = "some.module"
29 | mock_partner.GetClassname.return_value = "SomeAdapter"
30 |
31 | with patch('importlib.import_module') as mock_import:
32 | mock_module = MagicMock()
33 | mock_import.return_value = mock_module
34 | svc._set_iris_handles(mock_current, mock_partner)
35 |
36 | assert svc.iris_handle == mock_current
37 | assert svc.Adapter is not None
38 | assert svc.adapter is not None
39 |
40 | def test_dispatch_on_process_input(service):
41 | message = SimpleMessage(integer=1, string='test')
42 | service._dispatch_on_process_input(message)
43 |
44 | # Verify the message was processed
45 | service.iris_handle.dispatchOnProcessInput.assert_not_called()
46 |
47 | def test_custom_service():
48 | class CustomService(_BusinessService):
49 | def on_process_input(self, message):
50 | return SimpleMessage(integer=message.integer * 2, string=f"processed_{message.string}")
51 |
52 | service = CustomService()
53 | service.iris_handle = MagicMock()
54 |
55 | input_msg = SimpleMessage(integer=5, string='test')
56 | result = service.on_process_input(input_msg)
57 |
58 | assert isinstance(result, SimpleMessage)
59 | assert result.integer == 10
60 | assert result.string == "processed_test"
61 |
62 | def test_wait_for_next_call_interval(service):
63 | assert service._wait_for_next_call_interval is False
64 |
--------------------------------------------------------------------------------
/src/tests/test_cli.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from unittest.mock import patch
3 | from io import StringIO
4 | import json
5 | import os
6 | from iop._cli import main
7 | from iop._director import _Director
8 |
9 | class TestIOPCli(unittest.TestCase):
10 | """Test cases for IOP CLI functionality."""
11 |
12 | def test_help_and_basic_commands(self):
13 | """Test basic CLI commands like help and namespace."""
14 | # Test help
15 | with self.assertRaises(SystemExit) as cm:
16 | main(['-h'])
17 | self.assertEqual(cm.exception.code, 0)
18 |
19 | # Test without arguments
20 | with self.assertRaises(SystemExit) as cm:
21 | main([])
22 | self.assertEqual(cm.exception.code, 0)
23 |
24 | def test_default_settings(self):
25 | """Test default production settings."""
26 | # Test with name
27 | with self.assertRaises(SystemExit) as cm:
28 | main(['-d', 'UnitTest.Production'])
29 | self.assertEqual(cm.exception.code, 0)
30 | self.assertEqual(_Director.get_default_production(), 'UnitTest.Production')
31 |
32 | # Test without name
33 | with self.assertRaises(SystemExit) as cm:
34 | main(['-d'])
35 | self.assertEqual(cm.exception.code, 0)
36 |
37 | def test_production_controls(self):
38 | """Test production control commands (start, stop, restart, kill)."""
39 | # Test start
40 | with patch('iop._director._Director.start_production_with_log') as mock_start:
41 | with self.assertRaises(SystemExit) as cm:
42 | main(['-s', 'my_production'])
43 | self.assertEqual(cm.exception.code, 0)
44 | mock_start.assert_called_once_with('my_production')
45 |
46 | with patch('iop._director._Director.start_production') as mock_start:
47 | with self.assertRaises(SystemExit) as cm:
48 | main(['-s', 'my_production', '-D'])
49 | self.assertEqual(cm.exception.code, 0)
50 | mock_start.assert_called_once_with('my_production')
51 |
52 | # Test stop
53 | with patch('iop._director._Director.stop_production') as mock_stop:
54 | with patch('sys.stdout', new=StringIO()) as fake_out:
55 | with self.assertRaises(SystemExit) as cm:
56 | main(['-S'])
57 | self.assertEqual(cm.exception.code, 0)
58 | mock_stop.assert_called_once()
59 | self.assertEqual(fake_out.getvalue().strip(), 'Production UnitTest.Production stopped')
60 |
61 | # Test restart
62 | with patch('iop._director._Director.restart_production') as mock_restart:
63 | with self.assertRaises(SystemExit) as cm:
64 | main(['-r'])
65 | self.assertEqual(cm.exception.code, 0)
66 | mock_restart.assert_called_once()
67 |
68 | # Test kill
69 | with patch('iop._director._Director.shutdown_production') as mock_shutdown:
70 | with self.assertRaises(SystemExit) as cm:
71 | main(['-k'])
72 | self.assertEqual(cm.exception.code, 0)
73 | mock_shutdown.assert_called_once()
74 |
75 | def test_migration(self):
76 | """Test migration functionality."""
77 | # Test relative path
78 | with patch('iop._utils._Utils.migrate') as mock_migrate:
79 | with self.assertRaises(SystemExit) as cm:
80 | main(['-m', 'settings.json'])
81 | self.assertEqual(cm.exception.code, 0)
82 | mock_migrate.assert_called_once_with(os.path.join(os.getcwd(), 'settings.json'))
83 |
84 | # Test absolute path
85 | with patch('iop._utils._Utils.migrate') as mock_migrate:
86 | with self.assertRaises(SystemExit) as cm:
87 | main(['-m', '/tmp/settings.json'])
88 | self.assertEqual(cm.exception.code, 0)
89 | mock_migrate.assert_called_once_with('/tmp/settings.json')
90 |
91 | def test_initialization(self):
92 | """Test initialization command."""
93 | with patch('iop._utils._Utils.setup') as mock_setup:
94 | with self.assertRaises(SystemExit) as cm:
95 | main(['-i'])
96 | self.assertEqual(cm.exception.code, 0)
97 | mock_setup.assert_called_once_with(None)
98 |
99 | def test_component_testing(self):
100 | """Test component testing functionality."""
101 | # Test with ASCII
102 | with patch('iop._director._Director.test_component') as mock_test:
103 | with self.assertRaises(SystemExit) as cm:
104 | main(['-t', 'my_test', '-C', 'MyClass', '-B', 'my_body'])
105 | self.assertEqual(cm.exception.code, 0)
106 | mock_test.assert_called_once_with('my_test', classname='MyClass', body='my_body')
107 |
108 | # Test with Unicode
109 | with patch('iop._director._Director.test_component') as mock_test:
110 | with self.assertRaises(SystemExit) as cm:
111 | main(['-t', 'my_test', '-C', 'MyClass', '-B', 'あいうえお'])
112 | self.assertEqual(cm.exception.code, 0)
113 | mock_test.assert_called_once_with('my_test', classname='MyClass', body='あいうえお')
114 |
--------------------------------------------------------------------------------
/src/tests/test_commun.py:
--------------------------------------------------------------------------------
1 | import iris
2 | import os
3 | import sys
4 | import random
5 | import string
6 | import pytest
7 | from iop._message_validator import is_iris_object_instance, is_message_class, is_pickle_message_class
8 | from registerFilesIop.message import SimpleMessage, SimpleMessageNotMessage, PickledMessage
9 | from iop._common import _Common
10 |
11 | # Constants
12 | INSTALL_DIR = os.getenv('IRISINSTALLDIR', None) or os.getenv('ISC_PACKAGE_INSTALLDIR', None)
13 | MESSAGE_LOG_PATH = os.path.join(INSTALL_DIR, 'mgr', 'messages.log')
14 |
15 | @pytest.fixture
16 | def common():
17 | return _Common()
18 |
19 | @pytest.fixture
20 | def random_string(length=10):
21 | letters = string.ascii_lowercase
22 | return ''.join(random.choice(letters) for _ in range(length))
23 |
24 | @pytest.fixture
25 | def random_japanese(length=10):
26 | letters = 'あいうえお'
27 | return ''.join(random.choice(letters) for _ in range(length))
28 |
29 | class TestMessageClassification:
30 | def test_is_message_class(self):
31 | assert is_message_class(SimpleMessage) == True
32 | assert is_message_class(SimpleMessageNotMessage) == False
33 |
34 | def test_is_pickle_message_class(self):
35 | assert is_pickle_message_class(PickledMessage) == True
36 | assert is_pickle_message_class(SimpleMessageNotMessage) == False
37 |
38 | def test_is_iris_object_instance(self):
39 | msg = iris.cls('Ens.Request')._New()
40 | assert is_iris_object_instance(msg) == True
41 | assert is_iris_object_instance(SimpleMessageNotMessage) == False
42 |
43 | msg_job = iris.cls('Ens.Job')._New()
44 | assert is_iris_object_instance(msg_job) == False
45 |
46 | class TestLogging:
47 |
48 | def test_log_info_loggger(self, common, random_string):
49 | common.logger.info(random_string)
50 | rs = self._check_log_entry(random_string, 'test_log_info_loggger')
51 | for entry in rs:
52 | assert random_string in entry[9]
53 |
54 | def test_log_info_loggger_to_console(self, common, random_string):
55 | common.log_to_console = True
56 | common.logger.info(random_string)
57 |
58 | with open(MESSAGE_LOG_PATH, 'r') as file:
59 | last_line = file.readlines()[-1]
60 | assert random_string in last_line
61 |
62 | def test_log_info_to_console(self, common, random_string):
63 | common.log_to_console = True
64 | common.log_info(random_string)
65 |
66 | with open(MESSAGE_LOG_PATH, 'r') as file:
67 | last_line = file.readlines()[-1]
68 | assert random_string in last_line
69 |
70 | def test_log_info_to_console_from_method(self, common, random_string):
71 | common.trace(message=random_string, to_console=True)
72 |
73 | with open(MESSAGE_LOG_PATH, 'r') as file:
74 | last_line = file.readlines()[-1]
75 | assert random_string in last_line
76 |
77 | def _check_log_entry(self, message, method_name, level=4):
78 | sql = """
79 | SELECT * FROM Ens_Util.Log
80 | WHERE SourceClass = '_Common'
81 | AND SourceMethod = ?
82 | AND Text = ?
83 | AND Type = ?
84 | ORDER BY id DESC
85 | """
86 | stmt = iris.sql.prepare(sql)
87 | rs = stmt.execute(method_name, message, level)
88 | if rs is None:
89 | return []
90 | return rs
91 |
92 | def test_log_info(self, common, random_string):
93 | common.log_info(random_string)
94 | rs = self._check_log_entry(random_string, 'test_log_info')
95 | for entry in rs:
96 | assert random_string in entry[9]
97 |
98 | def test_log_warning(self, common, random_string):
99 | common.log_warning(random_string)
100 | rs = self._check_log_entry(random_string, 'test_log_info', 3)
101 | for entry in rs:
102 | assert random_string in entry[9]
103 |
104 | def test_log_info_japanese(self, common, random_japanese):
105 | common.log_info(random_japanese)
106 | rs = self._check_log_entry(random_japanese, 'test_log_info_japanese')
107 | for entry in rs:
108 | assert random_japanese in entry[9]
109 |
110 | class TestBusinessService:
111 | def test_get_info(self):
112 | path = os.path.dirname(os.path.realpath(__file__))
113 | sys.path.append(path)
114 |
115 | from registerFilesIop.edge.bs_underscore import BS
116 | result = BS._get_info()
117 | expected = ['iop.BusinessService', '', '', '', 'EnsLib.File.InboundAdapter']
118 |
119 | assert result == expected
120 |
--------------------------------------------------------------------------------
/src/tests/test_dtl.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | import iris
4 | import json
5 | from iop._utils import _Utils
6 | from registerFilesIop.message import SimpleMessage, ComplexMessage
7 |
8 | # Constants
9 | TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), 'cls')
10 |
11 | # Fixtures
12 | @pytest.fixture
13 | def load_cls_files():
14 | _Utils.raise_on_error(iris.cls('%SYSTEM.OBJ').LoadDir(TEST_DATA_DIR, 'cubk', "*.cls", 1))
15 |
16 | @pytest.fixture
17 | def iop_message():
18 | return iris.cls('IOP.Message')._New()
19 |
20 | # Test Data
21 | GET_VALUE_TEST_CASES = [
22 | ('{"string":"Foo", "integer":42}', 'string', 'Foo'),
23 | ('{"post":{"Title":"Foo"}, "string":"bar", "list_str":["Foo","Bar"]}', 'post.Title', 'Foo'),
24 | ('{"post":{"Title":"Foo"}, "list_post":[{"Title":"Bar"},{"Title":"Foo"}]}', 'list_post(2).Title', 'Foo'),
25 | ('{"list_str":["Foo","Bar"]}', 'list_str(2)', 'Bar'),
26 | ('{"list_str":["Foo","Bar"]}', 'list_str()', ['Foo','Bar']),
27 | ('{"list_str":["Foo","Bar"]}', 'list_str', ['Foo','Bar']),
28 | ('{"list":["Foo",["Bar","Baz"]]}', 'list(2)(2)', 'Baz'),
29 | ]
30 |
31 | SET_VALUE_TEST_CASES = [
32 | ('{"string":"Foo", "integer":42}', 'string', 'Bar', 'set', None, '{"string":"Bar", "integer":42}'),
33 | (r'{"post":{"Title":"Foo"}}', 'post.Title', 'Bar', 'set', None, r'{"post":{"Title":"Bar"}}'),
34 | (r'{}', 'post.Title', 'Bar', 'set', None, r'{"post":{"Title":"Bar"}}'),
35 | (r'{}', 'post()', 'Bar', 'append', None, r'{"post":["Bar"]}'),
36 | (r'{"post":["Foo"]}', 'post()', 'Bar', 'append', None, r'{"post":["Foo","Bar"]}'),
37 | ]
38 |
39 | TRANSFORM_TEST_CASES = [
40 | ('{"string":"Foo", "integer":42}', 'registerFilesIop.message.SimpleMessage', 'UnitTest.SimpleMessageGet', 'Foo'),
41 | ('{"post":{"Title":"Foo"}, "string":"bar", "list_str":["Foo","Bar"]}', 'registerFilesIop.message.ComplexMessage', 'UnitTest.ComplexGet', 'Foo'),
42 | ('{"post":{"Title":"Foo"}, "list_post":[{"Title":"Bar"},{"Title":"Foo"}]}', 'registerFilesIop.message.ComplexMessage', 'UnitTest.ComplexGetList', 'Foo'),
43 | ]
44 |
45 | # Tests
46 | class TestMessageSchema:
47 | @pytest.mark.parametrize("message_class,expected_name", [
48 | (SimpleMessage, f"{SimpleMessage.__module__}.{SimpleMessage.__name__}"),
49 | (ComplexMessage, f"{ComplexMessage.__module__}.{ComplexMessage.__name__}")
50 | ])
51 | def test_register_message_schema(self, message_class, expected_name):
52 | _Utils.register_message_schema(message_class)
53 | iop_schema = iris.cls('IOP.Message.JSONSchema')._OpenId(expected_name)
54 | assert iop_schema is not None
55 | assert iop_schema.Category == expected_name
56 | assert iop_schema.Name == expected_name
57 |
58 | class TestMessageOperations:
59 | @pytest.mark.parametrize("json_data,path,expected", GET_VALUE_TEST_CASES)
60 | def test_get_value_at(self, iop_message, json_data, path, expected):
61 | iop_message.json = json_data
62 | result = iop_message.GetValueAt(path)
63 | assert result == expected
64 |
65 | @pytest.mark.parametrize("json_data,path,value,action,key,expected_json", SET_VALUE_TEST_CASES)
66 | def test_set_value_at(self, iop_message, json_data, path, value, action, key, expected_json):
67 | iop_message.json = json_data
68 | iop_message.classname = 'foo'
69 | _Utils.raise_on_error(iop_message.SetValueAt(value, path, action, key))
70 | assert json.loads(iop_message.json) == json.loads(expected_json)
71 |
72 | class TestTransformations:
73 | @pytest.mark.parametrize("json_data,classname,transform_class,expected_value", TRANSFORM_TEST_CASES)
74 | def test_get_transform(self, load_cls_files, iop_message, json_data, classname, transform_class, expected_value):
75 | ref = iris.ref(None)
76 | iop_message.json = json_data
77 | iop_message.classname = classname
78 |
79 | iris.cls(transform_class).Transform(iop_message, ref)
80 | result = ref.value
81 |
82 | assert result.StringValue == expected_value
83 |
84 | def test_set_transform(self, load_cls_files):
85 | ref = iris.ref(None)
86 | message = iris.cls('Ens.StringRequest')._New()
87 | message.StringValue = 'Foo'
88 |
89 | _Utils.raise_on_error(iris.cls('UnitTest.SimpleMessageSet').Transform(message, ref))
90 | result = ref.value
91 |
92 | assert json.loads(result.json) == json.loads('{"string":"Foo"}')
93 |
94 | def test_set_transform_vdoc(self, load_cls_files, iop_message):
95 | ref = iris.ref(None)
96 | iop_message.json = '{"string":"Foo", "integer":42}'
97 | iop_message.classname = 'registerFilesIop.message.SimpleMessage'
98 |
99 | _Utils.raise_on_error(iris.cls('UnitTest.SimpleMessageSetVDoc').Transform(iop_message, ref))
100 | result = ref.value
101 |
102 | assert json.loads(result.json) == json.loads('{"string":"Foo", "integer":42}')
103 | assert result.classname == 'registerFilesIop.message.SimpleMessage'
104 |
--------------------------------------------------------------------------------
/src/tests/test_message.py:
--------------------------------------------------------------------------------
1 | import iris
2 |
3 | def test_iop_message_set_json():
4 | # test set_json
5 | iop_message = iris.cls('IOP.Message')._New()
6 | iop_message.json = 'test'
7 | assert iop_message.jstr.Read() == 'test'
8 | assert iop_message.type == 'String'
9 | assert iop_message.jsonString == 'test'
10 | assert iop_message.json == 'test'
--------------------------------------------------------------------------------
/src/tests/test_private_session.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from unittest.mock import MagicMock, patch
3 |
4 | from iop._private_session_duplex import _PrivateSessionDuplex
5 | from iop._private_session_process import _PrivateSessionProcess
6 | from registerFilesIop.message import SimpleMessage, MyResponse
7 |
8 | @pytest.fixture
9 | def duplex():
10 | duplex = _PrivateSessionDuplex()
11 | duplex.iris_handle = MagicMock()
12 | return duplex
13 |
14 | @pytest.fixture
15 | def process():
16 | process = _PrivateSessionProcess()
17 | process.iris_handle = MagicMock()
18 | return process
19 |
20 | class TestPrivateSessionDuplex:
21 | def test_set_iris_handles_with_iris_adapter(self, duplex):
22 | handle_current = MagicMock()
23 | handle_partner = MagicMock()
24 | handle_partner._IsA = MagicMock(return_value=True)
25 | handle_partner.GetModule = MagicMock(return_value="test_module")
26 | handle_partner.GetClassname = MagicMock(return_value="TestAdapter")
27 |
28 | with patch('importlib.import_module') as mock_import:
29 | mock_module = MagicMock()
30 | mock_adapter = MagicMock()
31 | mock_module.TestAdapter = mock_adapter
32 | mock_import.return_value = mock_module
33 |
34 | duplex._set_iris_handles(handle_current, handle_partner)
35 |
36 | assert duplex.iris_handle == handle_current
37 |
38 | def test_send_document_to_process(self, duplex):
39 | document = SimpleMessage(integer=1, string='test')
40 | duplex.iris_handle.dispatchSendDocumentToProcess = MagicMock(return_value=MyResponse(value='test'))
41 |
42 | result = duplex.send_document_to_process(document)
43 |
44 | duplex.iris_handle.dispatchSendDocumentToProcess.assert_called_once()
45 | assert isinstance(result, MyResponse)
46 | assert result.value == 'test'
47 |
48 | class TestPrivateSessionProcess:
49 | def test_dispatch_on_private_session_stopped(self, process):
50 | host_object = MagicMock()
51 | test_request = SimpleMessage(integer=1, string='test')
52 |
53 | with patch.object(process, 'on_private_session_stopped', return_value=MyResponse(value='test')) as mock_handler:
54 | result = process._dispatch_on_private_session_stopped(host_object, 'test_source', 'self_generated', test_request)
55 |
56 | mock_handler.assert_called_once()
57 | assert result.json == '{"value":"test"}'
58 |
59 | def test_dispatch_on_private_session_started(self, process):
60 | host_object = MagicMock()
61 | test_request = SimpleMessage(integer=1, string='test')
62 |
63 | with patch.object(process, 'on_private_session_started', return_value=MyResponse(value='test')) as mock_handler:
64 | result = process._dispatch_on_private_session_started(host_object, 'test_source', test_request)
65 |
66 | mock_handler.assert_called_once()
67 | assert result.json == '{"value":"test"}'
68 |
69 | def test_dispatch_on_document(self, process):
70 | host_object = MagicMock()
71 | test_request = SimpleMessage(integer=1, string='test')
72 |
73 | with patch.object(process, 'on_document', return_value=MyResponse(value='test')) as mock_handler:
74 | result = process._dispatch_on_document(host_object, 'test_source', test_request)
75 |
76 | mock_handler.assert_called_once()
77 | assert result.json == '{"value":"test"}'
78 |
--------------------------------------------------------------------------------
/src/tests/test_pydantic_message.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import decimal
3 | import uuid
4 | from typing import List, Optional
5 |
6 | import pytest
7 | from pydantic import BaseModel
8 |
9 | from iop._message import _PydanticMessage as PydanticMessage
10 | from iop._serialization import (
11 | serialize_message,
12 | deserialize_message,
13 | serialize_pickle_message,
14 | deserialize_pickle_message,
15 | )
16 |
17 | class SimpleModel(BaseModel):
18 | value: str
19 |
20 | class FullPydanticMessage(PydanticMessage):
21 | text: str
22 | dikt: dict
23 | text_json: str
24 | obj: SimpleModel
25 | number: int
26 | date: datetime.date
27 | time: datetime.time
28 | dt: datetime.datetime
29 | dec: decimal.Decimal
30 | uid: uuid.UUID
31 | data: bytes
32 | items: List[dict]
33 | optional_field: Optional[str] = None
34 |
35 | def test_pydantic_json_serialization():
36 | # Create test data
37 | test_items = [{'col1': 1, 'col2': 'a'}, {'col1': 2, 'col2': 'b'}]
38 | test_uuid = uuid.uuid4()
39 | test_bytes = b'hello world'
40 |
41 | msg = FullPydanticMessage(
42 | text="test",
43 | dikt={'key': 'value'},
44 | text_json="{\"key\": \"value\"}",
45 | obj=SimpleModel(value="test"),
46 | number=42,
47 | date=datetime.date(2023, 1, 1),
48 | time=datetime.time(12, 0),
49 | dt=datetime.datetime(2023, 1, 1, 12, 0),
50 | dec=decimal.Decimal("3.14"),
51 | uid=test_uuid,
52 | data=test_bytes,
53 | items=test_items
54 | )
55 |
56 | # Test serialization
57 | serial = serialize_message(msg)
58 | assert serial._IsA("IOP.Message")
59 | assert serial.classname == f"{FullPydanticMessage.__module__}.{FullPydanticMessage.__name__}"
60 |
61 | # Test deserialization
62 | result = deserialize_message(serial)
63 | assert isinstance(result, FullPydanticMessage)
64 | assert result.model_dump() == msg.model_dump()
65 |
66 | def test_pydantic_pickle_serialization():
67 | msg = FullPydanticMessage(
68 | text="test",
69 | dikt={'key': 'value'},
70 | text_json="{\"key\": \"value\"}",
71 | obj=SimpleModel(value="test"),
72 | number=42,
73 | date=datetime.date(2023, 1, 1),
74 | time=datetime.time(12, 0),
75 | dt=datetime.datetime(2023, 1, 1, 12, 0),
76 | dec=decimal.Decimal("3.14"),
77 | uid=uuid.uuid4(),
78 | data=b'hello world',
79 | items=[{'col1': 1, 'col2': 'a'}]
80 | )
81 |
82 | # Test serialization
83 | serial = serialize_pickle_message(msg)
84 | assert serial._IsA("IOP.PickleMessage")
85 | assert serial.classname == f"{FullPydanticMessage.__module__}.{FullPydanticMessage.__name__}"
86 |
87 | # Test deserialization
88 | result = deserialize_pickle_message(serial)
89 | assert isinstance(result, FullPydanticMessage)
90 | assert result.model_dump() == msg.model_dump()
91 |
92 | def test_optional_fields():
93 | # Test with optional field set
94 | msg1 = FullPydanticMessage(
95 | text="test",
96 | dikt={},
97 | text_json="{}",
98 | obj=SimpleModel(value="test"),
99 | number=42,
100 | date=datetime.date(2023, 1, 1),
101 | time=datetime.time(12, 0),
102 | dt=datetime.datetime(2023, 1, 1, 12, 0),
103 | dec=decimal.Decimal("3.14"),
104 | uid=uuid.uuid4(),
105 | data=b'hello',
106 | items=[],
107 | optional_field="present"
108 | )
109 |
110 | # Test with optional field not set
111 | msg2 = FullPydanticMessage(
112 | text="test",
113 | dikt={},
114 | text_json="{}",
115 | obj=SimpleModel(value="test"),
116 | number=42,
117 | date=datetime.date(2023, 1, 1),
118 | time=datetime.time(12, 0),
119 | dt=datetime.datetime(2023, 1, 1, 12, 0),
120 | dec=decimal.Decimal("3.14"),
121 | uid=uuid.uuid4(),
122 | data=b'hello',
123 | items=[]
124 | )
125 |
126 | # Test both serialization methods for each message
127 | for msg in [msg1, msg2]:
128 | for serialize_fn, deserialize_fn in [
129 | (serialize_message, deserialize_message),
130 | (serialize_pickle_message, deserialize_pickle_message)
131 | ]:
132 | serial = serialize_fn(msg)
133 | result = deserialize_fn(serial)
134 | assert isinstance(result, FullPydanticMessage)
135 | assert result.model_dump() == msg.model_dump()
136 |
--------------------------------------------------------------------------------
/test-in-docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | iris_start () {
4 | iris start iris
5 |
6 | # Merge cpf file
7 | iris merge iris merge.cpf
8 | }
9 |
10 | iris_stop () {
11 | echo "Stopping IRIS"
12 | iris stop iris quietly
13 | }
14 |
15 | exit_on_error () {
16 | exit=$?;
17 | if [ $exit -ne 0 ]; then
18 | iris_stop
19 | exit $exit
20 | fi
21 | }
22 |
23 | iris_start
24 |
25 | cd src
26 |
27 | # print iris version
28 | echo "IRIS version:"
29 | python3 -c "import iris; print(iris.system.Version.GetVersion())"
30 |
31 | # setup the environment
32 | python3 -m iop --init
33 | exit_on_error
34 |
35 | # Unit tests
36 | cd ..
37 | python3 -m pytest
38 | exit_on_error
39 |
40 | # Integration tests
41 | cd src
42 | python3 -m iop --migrate ../demo/python/reddit/settings.py
43 | exit_on_error
44 |
45 | python3 -m iop --default PEX.Production
46 | exit_on_error
47 |
48 | python3 -m iop --start PEX.Production --detach
49 | exit_on_error
50 |
51 | python3 -m iop --log 10
52 |
53 | python3 -m iop -S
54 | exit_on_error
55 |
56 | iris_stop
--------------------------------------------------------------------------------