├── .devcontainer └── devcontainer.json ├── .dockerignore ├── .gitattributes ├── .github └── workflows │ ├── docs.yml │ ├── publish.yml │ └── pytest-iris.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── build-dist.sh ├── demo ├── iris │ └── PEX │ │ ├── Bench │ │ ├── BO.cls │ │ ├── BP.cls │ │ ├── HugeFile.cls │ │ └── MSG.cls │ │ ├── Demo │ │ └── PostMessage.cls │ │ ├── Production.cls │ │ ├── Reddit │ │ ├── InboundAdapter.cls │ │ └── Post.cls │ │ └── WSGI.cls └── python │ ├── async │ ├── bo.py │ ├── bp.py │ ├── msg.py │ └── settings.py │ ├── async_ng │ ├── bo.py │ ├── bp.py │ ├── msg.py │ └── settings.py │ ├── bench │ ├── bo.py │ ├── bp.py │ └── msg.py │ ├── duplex │ ├── adapter.py │ ├── bo.py │ ├── bp.py │ └── bs.py │ ├── multi_sync │ ├── bo.py │ ├── bp.py │ ├── msg.py │ └── settings.py │ ├── reddit │ ├── adapter.py │ ├── app_flask.py │ ├── bo.py │ ├── bp.py │ ├── bs.py │ ├── message.py │ ├── obj.py │ └── settings.py │ └── requirements.txt ├── docker-compose.yml ├── dockerfile-ci ├── docs ├── benchmarks.md ├── changelog.md ├── code-of-conduct.md ├── command-line.md ├── contributing.md ├── credits.md ├── debug.md ├── dtl.md ├── example.md ├── getting-started │ ├── first-steps.md │ ├── installation.md │ └── register-component.md ├── img │ ├── IoPRemoteDebug.mp4 │ ├── RemoteDebugOptions.png │ ├── RemoteDebugSuccess.png │ ├── RemoteDebugToLate.png │ ├── RemoteDebugWaiting.png │ ├── complex_transform.png │ ├── custom_settings.png │ ├── dtl_wizard.png │ ├── interop_dtl_management_portal.png │ ├── settings-in-production.png │ ├── test_dtl.png │ ├── traceback_disable.png │ ├── traceback_enable.png │ ├── vdoc_type.png │ ├── vscode_breakpoint.png │ ├── vscode_debug.png │ ├── vscode_debugging.png │ ├── vscode_open.png │ ├── vscode_python_extension.png │ └── vscode_select_venv.png ├── index.md ├── logging.md ├── prod-settings.md ├── python-api.md └── useful-links.md ├── entrypoint.sh ├── key └── .gitignore ├── merge.cpf ├── misc ├── component-config.png ├── interop-screenshot.png └── json-message-trace.png ├── mkdocs.yml ├── module.xml ├── output └── .gitignore ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── setup.py ├── src ├── grongier │ ├── __init__.py │ ├── cls │ │ └── Grongier │ │ │ ├── PEX │ │ │ ├── BusinessOperation.cls │ │ │ ├── BusinessProcess.cls │ │ │ ├── BusinessService.cls │ │ │ ├── Common.cls │ │ │ ├── Director.cls │ │ │ ├── Duplex │ │ │ │ ├── Operation.cls │ │ │ │ ├── Process.cls │ │ │ │ └── Service.cls │ │ │ ├── InboundAdapter.cls │ │ │ ├── Message.cls │ │ │ ├── OutboundAdapter.cls │ │ │ ├── PickleMessage.cls │ │ │ ├── PrivateSession │ │ │ │ ├── Duplex.cls │ │ │ │ └── Message │ │ │ │ │ ├── Ack.cls │ │ │ │ │ ├── Poll.cls │ │ │ │ │ ├── Start.cls │ │ │ │ │ └── Stop.cls │ │ │ ├── Test.cls │ │ │ └── Utils.cls │ │ │ └── Service │ │ │ └── WSGI.cls │ └── pex │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── _business_host.py │ │ ├── _cli.py │ │ ├── _common.py │ │ ├── _director.py │ │ ├── _utils.py │ │ └── wsgi │ │ └── handlers.py ├── iop │ ├── __init__.py │ ├── __main__.py │ ├── _async_request.py │ ├── _business_host.py │ ├── _business_operation.py │ ├── _business_process.py │ ├── _business_service.py │ ├── _cli.py │ ├── _common.py │ ├── _debugpy.py │ ├── _decorators.py │ ├── _director.py │ ├── _dispatch.py │ ├── _inbound_adapter.py │ ├── _iris.py │ ├── _log_manager.py │ ├── _message.py │ ├── _message_validator.py │ ├── _outbound_adapter.py │ ├── _private_session_duplex.py │ ├── _private_session_process.py │ ├── _serialization.py │ ├── _utils.py │ ├── cls │ │ └── IOP │ │ │ ├── BusinessOperation.cls │ │ │ ├── BusinessProcess.cls │ │ │ ├── BusinessService.cls │ │ │ ├── Common.cls │ │ │ ├── Director.cls │ │ │ ├── Duplex │ │ │ ├── Operation.cls │ │ │ ├── Process.cls │ │ │ └── Service.cls │ │ │ ├── InboundAdapter.cls │ │ │ ├── Message.cls │ │ │ ├── Message │ │ │ └── JSONSchema.cls │ │ │ ├── OutboundAdapter.cls │ │ │ ├── PickleMessage.cls │ │ │ ├── PrivateSession │ │ │ ├── Duplex.cls │ │ │ └── Message │ │ │ │ ├── Ack.cls │ │ │ │ ├── Poll.cls │ │ │ │ ├── Start.cls │ │ │ │ └── Stop.cls │ │ │ ├── Service │ │ │ └── WSGI.cls │ │ │ ├── Test.cls │ │ │ └── Utils.cls │ └── wsgi │ │ └── handlers.py └── tests │ ├── bench │ ├── bench_bo.py │ ├── bench_bp.py │ ├── cls │ │ ├── Bench.Operation.cls │ │ └── Bench.Process.cls │ ├── msg.py │ └── settings.py │ ├── cls │ ├── ComplexGet.cls │ ├── ComplexGetList.cls │ ├── ComplexTransform.cls │ ├── SimpleMessageGet.cls │ ├── SimpleMessageSet.cls │ └── SimpleMessageSetVDoc.cls │ ├── conftest.py │ ├── registerFilesIop │ ├── adapter.py │ ├── bo.py │ ├── bp.py │ ├── bs.py │ ├── edge │ │ ├── bs-dash.py │ │ └── bs_underscore.py │ ├── message.py │ ├── obj.py │ └── settings.py │ ├── test_adapters.py │ ├── test_bench.py │ ├── test_bproduction_settings.py │ ├── test_business_host.py │ ├── test_business_operation.py │ ├── test_business_process.py │ ├── test_business_service.py │ ├── test_cli.py │ ├── test_commun.py │ ├── test_director.py │ ├── test_dispatch.py │ ├── test_dtl.py │ ├── test_message.py │ ├── test_private_session.py │ ├── test_pydantic_message.py │ ├── test_serialization.py │ └── test_utils.py └── test-in-docker.sh /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.224.2/containers/docker-existing-docker-compose 3 | { 4 | "name": "iris-python-template devcontainer", 5 | 6 | // Use the same recipe as creates the container we use when working locally. 7 | "dockerComposeFile": [ 8 | "../docker-compose.yml" 9 | ], 10 | 11 | "service": "iris", 12 | 13 | "workspaceFolder": "/irisdev/app", 14 | 15 | // This provides the elements of the connection object which require different values when connecting to the workspace within the container, 16 | // versus those in .vscode/settings.json which apply when operating locally on the workspace files. 17 | // We define and use a `server` so that (a) a user-level `objectscript.conn.server` properly doesn't override us, and (b) so InterSystems 18 | // Server Manager can also be used. 19 | "settings": { 20 | "objectscript.conn" :{ 21 | "server": "devcontainer", 22 | "active": true, 23 | }, 24 | "intersystems.servers": { 25 | "devcontainer": { 26 | "username": "SuperUser", 27 | "password": "SYS", 28 | "webServer": { 29 | "scheme": "http", 30 | "host": "127.0.0.1", 31 | "port": 52773 32 | }, 33 | }, 34 | }, 35 | }, 36 | 37 | // Add the IDs of extensions we want installed when the container is created. 38 | // Currently (March 2022) `intersystems.language-server` fails to run within the container (alpine platform). 39 | // Issue is probably https://github.com/intersystems/language-server/issues/185 and/or https://github.com/intersystems/language-server/issues/32 40 | // Crash gets reported to the user, after which `intersystems-community.vscode-objectscript` falls back to 41 | // using its TextMate grammar for code coloring. 42 | "extensions": [ 43 | "ms-python.python", 44 | "ms-python.vscode-pylance", 45 | "intersystems-community.vscode-objectscript", 46 | "intersystems.language-server", 47 | "intersystems-community.servermanager", 48 | ], 49 | } 50 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | .git 3 | .venv -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh text eol=lf 2 | *.cls text eol=lf 3 | *.mac text eol=lf 4 | *.int text eol=lf 5 | Dockerfil* text eol=lf -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Docs 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # This workflow contains a single job called "build" 17 | build: 18 | # The type of runner that the job will run on 19 | runs-on: ubuntu-latest 20 | 21 | # Steps represent a sequence of tasks that will be executed as part of the job 22 | steps: 23 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 24 | - uses: actions/checkout@v2 25 | with: 26 | fetch-depth: 0 27 | - uses: actions/setup-python@v2 28 | - run: pip install --upgrade pip && pip install mkdocs mkdocs-gen-files pymdown-extensions 29 | - run: git config user.name 'github-actions[bot]' && git config user.email 'github-actions[bot]@users.noreply.github.com' 30 | - name: Publish docs 31 | run: mkdocs gh-deploy -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | push: 15 | branches: 16 | - master 17 | paths-ignore: 18 | - 'docs/**' 19 | - 'tests/**' 20 | - 'demo/**' 21 | - '.vscode/**' 22 | - '.github/**' 23 | 24 | jobs: 25 | deploy: 26 | 27 | runs-on: ubuntu-latest 28 | permissions: 29 | id-token: write 30 | contents: write 31 | 32 | steps: 33 | - uses: actions/checkout@v3 34 | - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* 35 | if: github.event_name == 'push' 36 | - name: Set up Python 37 | uses: actions/setup-python@v3 38 | with: 39 | python-version: '3.10' 40 | - name: Install dependencies 41 | id: set-version 42 | run: | 43 | VERSION=$(grep version pyproject.toml | tr -s ' ' | tr -d '"' | tr -d "'" | cut -d' ' -f3) 44 | [ $GITHUB_EVENT_NAME == 'release' ] && VERSION=${{ github.event.release.tag_name }} && VERSION=${VERSION/v/} 45 | [ $GITHUB_EVENT_NAME == 'push' ] && VERSION+=b && VERSION+=$(($(git tag -l "*$VERSION*" | cut -db -f2 | sort -n | tail -1)+1)) 46 | sed -ie "s/version = .*/version = \"$VERSION\"/" pyproject.toml 47 | python -m pip install --upgrade pip 48 | pip install -U pip setuptools 49 | pip install -r requirements-dev.txt 50 | echo version=$VERSION >> $GITHUB_OUTPUT 51 | NAME="iris_pex_embedded_python"-${VERSION}-py3-none-any 52 | echo name=$NAME >> $GITHUB_OUTPUT 53 | - name: Build package 54 | run: ./build-dist.sh 55 | - name: Publish package 56 | uses: pypa/gh-action-pypi-publish@release/v1 57 | with: 58 | skip_existing: true 59 | - name: Create Beta Release 60 | id: create_release 61 | uses: softprops/action-gh-release@v1 62 | with: 63 | tag_name: v${{ steps.set-version.outputs.version }} 64 | prerelease: ${{ github.event_name != 'release' }} 65 | files: dist/${{ steps.set-version.outputs.name }}.whl 66 | - uses: actions/checkout@v3 67 | if: github.event_name == 'release' 68 | with: 69 | ref: master 70 | - name: Bump version 71 | if: github.event_name == 'release' 72 | run: | 73 | git config --global user.name 'ProjectBot' 74 | git config --global user.email 'bot@users.noreply.github.com' 75 | VERSION=${{ github.event.release.tag_name }} && VERSION=${VERSION/v/} 76 | VERSION=`echo $VERSION | awk -F. '/[0-9]+\./{$NF++;print}' OFS=.` 77 | sed -ie "s/version = .*/version = \"$VERSION\"/" pyproject.toml 78 | git add pyproject.toml 79 | git commit -m 'auto bump version with release' 80 | git push -------------------------------------------------------------------------------- /.github/workflows/pytest-iris.yml: -------------------------------------------------------------------------------- 1 | name: pytest-iris 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'README.md' 7 | - '.github/**' 8 | 9 | 10 | jobs: 11 | test: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | image: 16 | - intersystemsdc/iris-community:latest 17 | - intersystemsdc/iris-community:preview 18 | runs-on: ubuntu-latest 19 | env: 20 | IMAGE: ${{ matrix.image }} 21 | steps: 22 | - uses: actions/checkout@v3 23 | - run: docker pull $IMAGE 24 | - name: Run Tests 25 | run: | 26 | docker build --build-arg BASE=$IMAGE -t pytest-iris -f dockerfile-ci . 27 | docker run -i --rm pytest-iris 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__/ 3 | 4 | src/python/grongier_pex.egg-info/dependency_links.txt 5 | src/python/grongier_pex.egg-info/top_level.txt 6 | src/python/grongier_pex.egg-info/SOURCES.txt 7 | src/python/grongier_pex.egg-info/PKG-INFO 8 | src/python/build/** 9 | iris-main.log 10 | 11 | .venv 12 | .venv-iris 13 | *.egg-info 14 | dist 15 | build 16 | waitISC.log 17 | .env 18 | 19 | prof 20 | src/tests/bench/result.txt 21 | .coverage 22 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-vscode-remote.remote-containers", 4 | "ms-vscode-remote.vscode-remote-extensionpack", 5 | "intersystems-community.vscode-objectscript", 6 | "intersystems.language-server" 7 | ] 8 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Unit Test", 6 | "type": "python", 7 | "request": "test", 8 | "justMyCode": false, 9 | }, 10 | { 11 | "name": "Python: Current File", 12 | "type": "python", 13 | "request": "launch", 14 | "program": "${file}", 15 | "console": "integratedTerminal", 16 | "justMyCode": false 17 | }, 18 | { 19 | "type": "objectscript", 20 | "request": "launch", 21 | "name": "ObjectScript Debug Class", 22 | "program": "##class(Grongier.PEX.Utils).Test()", 23 | }, 24 | { 25 | "type": "objectscript", 26 | "request": "attach", 27 | "name": "ObjectScript Attach", 28 | "processId": "${command:PickProcess}", 29 | "system": true 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | 4 | "Dockerfile*": "dockerfile", 5 | "iris.script": "objectscript" 6 | }, 7 | "objectscript.conn" :{ 8 | "ns": "IRISAPP", 9 | "username": "_SYSTEM", 10 | "password": "SYS", 11 | "docker-compose": { 12 | "service": "iris", 13 | "internalPort": 52773 14 | }, 15 | "active": true, 16 | "links": { 17 | "Production": "http://localhost:${port}/csp/irisapp/EnsPortal.ProductionConfig.zen?PRODUCTION=PEX.Production" 18 | }, 19 | "server": "iris" 20 | }, 21 | "sqltools.connections": [ 22 | { 23 | "namespace": "IRISAPP", 24 | "connectionMethod": "Server and Port", 25 | "showSystem": false, 26 | "previewLimit": 50, 27 | "server": "localhost", 28 | "port": 52795, 29 | "askForPassword": false, 30 | "driver": "InterSystems IRIS", 31 | "name": "IRISAPP", 32 | "username": "_SYSTEM", 33 | "password": "SYS" 34 | } 35 | ] 36 | 37 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG IMAGE=intersystemsdc/iris-community:latest 2 | FROM $IMAGE 3 | 4 | WORKDIR /irisdev/app 5 | 6 | ## Python stuff 7 | ENV IRISUSERNAME "SuperUser" 8 | ENV IRISPASSWORD "SYS" 9 | ENV IRISNAMESPACE "IRISAPP" 10 | ENV PYTHON_PATH=/usr/irissys/bin/ 11 | ENV LD_LIBRARY_PATH=${ISC_PACKAGE_INSTALLDIR}/bin:${LD_LIBRARY_PATH} 12 | ENV PATH "/home/irisowner/.local/bin:/usr/irissys/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/irisowner/bin" 13 | 14 | COPY . . 15 | 16 | RUN pip install -r requirements-dev.txt 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 InterSystems Developer Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IoP (Interoperability On Python) 2 | 3 | [![PyPI - Status](https://img.shields.io/pypi/status/iris-pex-embedded-python)](https://pypi.org/project/iris-pex-embedded-python/) 4 | [![PyPI](https://img.shields.io/pypi/v/iris-pex-embedded-python)](https://pypi.org/project/iris-pex-embedded-python/) 5 | [![PyPI - Downloads](https://img.shields.io/pypi/dm/iris-pex-embedded-python)](https://pypi.org/project/iris-pex-embedded-python/) 6 | [![PyPI - License](https://img.shields.io/pypi/l/iris-pex-embedded-python)](https://pypi.org/project/iris-pex-embedded-python/) 7 | ![GitHub last commit](https://img.shields.io/github/last-commit/grongierisc/interoperability-embedded-python) 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 | Documentation can be found [here](https://grongierisc.github.io/interoperability-embedded-python/). 12 | 13 | ## Example 14 | 15 | Here's a simple example of how a Business Operation can be implemented in Python: 16 | 17 | ```python 18 | from iop import BusinessOperation 19 | 20 | class MyBo(BusinessOperation): 21 | def on_message(self, request): 22 | self.log_info("Hello World") 23 | ``` 24 | 25 | ## Installation 26 | 27 | To start using this proof of concept, install it using pip: 28 | 29 | ```bash 30 | pip install iris-pex-embedded-python 31 | ``` 32 | 33 | ## Getting Started 34 | 35 | If you're new to this project, begin by reading the [installation guide](https://grongierisc.github.io/interoperability-embedded-python/getting-started/installation). Then, follow the [first steps](https://grongierisc.github.io/interoperability-embedded-python/getting-started/first-steps) to create your first Business Operation. 36 | 37 | Happy coding! -------------------------------------------------------------------------------- /build-dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf ./build 4 | 5 | set -eo pipefail 6 | 7 | PYTHON_BIN=${PYTHON_BIN:-python3} 8 | 9 | echo "$PYTHON_BIN" 10 | 11 | set -x 12 | 13 | cd "$PROJECT" 14 | $PYTHON_BIN setup.py sdist bdist_wheel 15 | 16 | set +x -------------------------------------------------------------------------------- /demo/iris/PEX/Bench/BO.cls: -------------------------------------------------------------------------------- 1 | Class PEX.Bench.BO Extends Ens.BusinessOperation 2 | { 3 | 4 | Parameter INVOCATION = "Queue"; 5 | 6 | Method SampleCall(msg As Ens.Request, Output pResponse As Ens.Response) As %Status 7 | { 8 | set dump = msg.PropertyName0 9 | set dump = msg.PropertyName1 10 | set dump = msg.PropertyName2 11 | set dump = msg.PropertyName1 12 | set dump = msg.PropertyName3 13 | set dump = msg.PropertyName4 14 | set dump = msg.PropertyName5 15 | set dump = msg.PropertyName6 16 | set dump = msg.PropertyName7 17 | set dump = msg.PropertyName8 18 | set dump = msg.PropertyName9 19 | Return ##class(Ens.Response).%New() 20 | } 21 | 22 | XData MessageMap 23 | { 24 | 25 | 26 | SampleCall 27 | 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /demo/iris/PEX/Bench/BP.cls: -------------------------------------------------------------------------------- 1 | /// Description 2 | Class PEX.Bench.BP Extends Ens.BusinessProcess [ ClassType = persistent, ProcedureBlock ] 3 | { 4 | 5 | Method OnRequest(pRequest As Ens.Request, Output pResponse As Ens.Response) As %Status 6 | { 7 | set sc =$$$OK 8 | 9 | set start = $P($ZTS,",",2) 10 | 11 | For i = 1:1:1000 { 12 | set msg = ##class(PEX.Bench.MSG).%New() 13 | 14 | set msg.PropertyName0 = "PropertyName0" 15 | set msg.PropertyName1 = "PropertyName0" 16 | set msg.PropertyName2 = "PropertyName0" 17 | set msg.PropertyName3 = "PropertyName0" 18 | set msg.PropertyName4 = "PropertyName0" 19 | set msg.PropertyName5 = "PropertyName0" 20 | set msg.PropertyName6 = "PropertyName0" 21 | set msg.PropertyName7 = "PropertyName0" 22 | set msg.PropertyName8 = "PropertyName0" 23 | set msg.PropertyName9 = "PropertyName0" 24 | 25 | $$$ThrowOnError(..SendRequestSync("PEX.Bench.BO",msg)) 26 | 27 | Write i, ! 28 | } 29 | 30 | s end=$P($ZTS,",",2) 31 | 32 | $$$LOGINFO(end-start) 33 | 34 | set pResponse = ##class(Ens.StringResponse).%New(end-start) 35 | 36 | Return sc 37 | } 38 | 39 | Storage Default 40 | { 41 | %Storage.Persistent 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /demo/iris/PEX/Bench/HugeFile.cls: -------------------------------------------------------------------------------- 1 | Class PEX.Bench.HugeFile 2 | { 3 | 4 | ClassMethod HugeFile(file As %String) As %Status 5 | { 6 | Set sc = $$$OK 7 | set tjson = ##class(%Stream.GlobalCharacter).%New() 8 | d file.%ToJSON(tjson) 9 | set dyna = {}.%FromJSON(tjson) 10 | Return sc 11 | } 12 | 13 | /// Description 14 | ClassMethod ReadFile() As %Status 15 | { 16 | Set sc = $$$OK 17 | 18 | #; #; Set tFile = ##class(%File).%New("/tmp/test") 19 | 20 | #; Do tFile.Open("RU") 21 | 22 | #; set str = "" 23 | #; set len = 1000 24 | 25 | #; set dyna = {}.%FromJSON(tFile) 26 | #; do dyna.%Set("stream",##class(%Stream.DynamicCharacter).%New("test"),"stream") 27 | 28 | set maxstr=$tr($j("",$$$MaxStringLength)," ","X") 29 | set maxstr=maxstr_"A" 30 | 31 | set stream = ##class(%Stream.DynamicCharacter).%New() 32 | do stream.Write(maxstr) 33 | do stream.Write(maxstr) 34 | 35 | do ##class(PEX.Bench.HugeFile).HugeFile(stream) 36 | 37 | Return sc 38 | } 39 | 40 | /// test 41 | ClassMethod test() As %Status 42 | { 43 | Set sc = $$$OK 44 | set maxString = $tr($j("",$$$MaxStringLength)," ","X") 45 | 46 | set stream = ##class(%Stream.GlobalCharacter).%New() 47 | do stream.Write(maxString) 48 | do stream.Write(maxString) 49 | do stream.Write("endmax") 50 | 51 | set dyna = {} 52 | set dyna.maxString = maxString 53 | set dyna.hugeString = stream 54 | do dyna.%Set("hugeString", stream,"stream") 55 | 56 | do ##class(PEX.Bench.HugeFile).HugeFile(dyna) 57 | 58 | Return sc 59 | } 60 | 61 | ClassMethod MaxString() As %String 62 | { 63 | quit $tr($j("",$$$MaxStringLength)," ","X") 64 | } 65 | 66 | ClassMethod Python2() As %Status [ Language = python ] 67 | { 68 | import iris 69 | stream = iris.cls('%Stream.GlobalCharacter')._New() 70 | stream.Write(iris.cls('PEX.Bench.HugeFile').MaxString()) 71 | stream.Write(iris.cls('PEX.Bench.HugeFile').MaxString()) 72 | iris.cls('PEX.Bench.HugeFile').HugeFile(stream) 73 | } 74 | 75 | ClassMethod Python() As %Status [ Language = python ] 76 | { 77 | file_path = '/irisdev/app/misc/mov.mkv' 78 | f = open(file_path, 'rb') 79 | file_content = f.read() 80 | f.close() 81 | import iris 82 | from grongier.pex import BusinessProcess,Message 83 | bp = BusinessProcess() 84 | msg = Message() 85 | msg.bytes = file_content 86 | request = bp._serialize(msg) 87 | i = request.find(":") 88 | dyna = iris.cls('%DynamicObject')._New() 89 | dyna._Set('stream','request[i+1:]') 90 | with open('/tmp/test','w') as f: 91 | f.write(request[i+1:]) 92 | iris.cls('PEX.Bench.HugeFile').HugeFile(dyna) 93 | } 94 | 95 | ClassMethod CosReturnError() 96 | { 97 | q $SYSTEM.Status.Error(101,"5","10","2.7") 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /demo/iris/PEX/Bench/MSG.cls: -------------------------------------------------------------------------------- 1 | Class PEX.Bench.MSG Extends Ens.Request 2 | { 3 | 4 | Property PropertyName0 As %String; 5 | 6 | Property PropertyName1 As %String; 7 | 8 | Property PropertyName2 As %String; 9 | 10 | Property PropertyName3 As %String; 11 | 12 | Property PropertyName4 As %String; 13 | 14 | Property PropertyName5 As %String; 15 | 16 | Property PropertyName6 As %String; 17 | 18 | Property PropertyName7 As %String; 19 | 20 | Property PropertyName8 As %String; 21 | 22 | Property PropertyName9 As %String; 23 | 24 | Storage Default 25 | { 26 | 27 | "MSG" 28 | 29 | PropertyName0 30 | 31 | 32 | PropertyName1 33 | 34 | 35 | PropertyName2 36 | 37 | 38 | PropertyName3 39 | 40 | 41 | PropertyName4 42 | 43 | 44 | PropertyName5 45 | 46 | 47 | PropertyName6 48 | 49 | 50 | PropertyName7 51 | 52 | 53 | PropertyName8 54 | 55 | 56 | PropertyName9 57 | 58 | 59 | MSGDefaultData 60 | %Storage.Persistent 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /demo/iris/PEX/Demo/PostMessage.cls: -------------------------------------------------------------------------------- 1 | Class dc.Demo.PostMessage Extends Ens.Request 2 | { 3 | 4 | Property ToEmailAddress As %String; 5 | 6 | Property Found As %String; 7 | 8 | Property Post As dc.Reddit.Post; 9 | 10 | Storage Default 11 | { 12 | 13 | "PostMessage" 14 | 15 | ToEmailAddress 16 | 17 | 18 | Post 19 | 20 | 21 | Found 22 | 23 | 24 | PostMessageDefaultData 25 | %Storage.Persistent 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /demo/iris/PEX/Production.cls: -------------------------------------------------------------------------------- 1 | Class PEX.Production Extends Ens.Production 2 | { 3 | 4 | XData ProductionDefinition 5 | { 6 | 7 | 8 | 2 9 | 10 | utf-8 11 | /irisdev/app/output/ 12 | path=/irisdev/app/output/ 13 | 14 | 15 | utf-8 16 | /irisdev/app/output/ 17 | path=/irisdev/app/output/ 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | limit=10 35 | 36 | 37 | /new/ 38 | 4 39 | default 40 | 41 | 42 | limit=3 43 | 44 | 45 | 46 | 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /demo/iris/PEX/Reddit/InboundAdapter.cls: -------------------------------------------------------------------------------- 1 | Class dc.Reddit.InboundAdapter Extends Ens.InboundAdapter 2 | { 3 | 4 | Property LastPostName As %String; 5 | 6 | Property SSLConfig As %String; 7 | 8 | Parameter SERVER = "www.reddit.com"; 9 | 10 | Property Feed As %String; 11 | 12 | /// how many messages to receive per request 13 | Property Limit As %Integer; 14 | 15 | Parameter SETTINGS = "SSLConfig:Connection:sslConfigSelector,Feed:Connection,Limit:Connection"; 16 | 17 | Method OnInit() As %Status 18 | { 19 | If (..SSLConfig = "") { 20 | return $$$ERROR(5001, "SSLConfig required") 21 | } 22 | If (..Feed="") { 23 | Set ..Feed = "/new/" 24 | } 25 | Set ..LastPostName = "" 26 | 27 | Quit $$$OK 28 | } 29 | 30 | Method OnTask() As %Status 31 | { 32 | $$$TRACE("LIMIT:"_..Limit) 33 | If ((..SSLConfig="") || (..Feed="")) { 34 | Return $$$OK 35 | } 36 | Set tSC = 1 37 | // HTTP Request 38 | #dim httprequest as %Net.HttpRequest 39 | #dim httpResponse as %Net.HttpResponse 40 | Try { 41 | Set httprequest = ##class(%Net.HttpRequest).%New() 42 | Do httprequest.ServerSet(..#SERVER) 43 | Do httprequest.SSLConfigurationSet(..SSLConfig) 44 | Set requestString = ..Feed_".json?before="_..LastPostName_"&limit="_..Limit 45 | $$$TRACE(requestString) 46 | Do httprequest.Get(requestString) 47 | Set httpResponse = httprequest.HttpResponse 48 | If (httpResponse.StatusCode '=200) { 49 | $$$ThrowStatus($$$ERROR(5001, "HTTP StatusCode = "_httpResponse.StatusCode)) 50 | } 51 | Set jo = {}.%FromJSON(httpResponse.Data) 52 | Set iter = jo.data.children.%GetIterator() 53 | Set updateLast = 0 54 | While iter.%GetNext(.key , .value ) { 55 | If (value.data.selftext="") { Continue } 56 | Set post = ##class(dc.Reddit.Post).%New() 57 | $$$ThrowOnError(post.%JSONImport(value.data)) 58 | Set post.OriginalJSON = value.%ToJSON() 59 | $$$ThrowOnError(post.%Save()) 60 | If ('updateLast) { 61 | Set ..LastPostName = value.data.name 62 | Set updateLast = 1 63 | } 64 | $$$ThrowOnError(..BusinessHost.ProcessInput(post)) 65 | } 66 | } Catch ex { 67 | Do ex.Log() 68 | Set tSC = ex.AsStatus() 69 | } 70 | Set ..BusinessHost.%WaitForNextCallInterval=1 71 | Quit tSC 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /demo/iris/PEX/Reddit/Post.cls: -------------------------------------------------------------------------------- 1 | Class dc.Reddit.Post Extends (%Persistent, %JSON.Adaptor, %XML.Adaptor) 2 | { 3 | 4 | Parameter %JSONIGNOREINVALIDFIELD = 1; 5 | 6 | Property OriginalJSON As %String(%JSONINCLUDE = "none", MAXLEN = 3000000); 7 | 8 | Property Title As %String(%JSONFIELDNAME = "title", MAXLEN = 255); 9 | 10 | Property Selftext As %String(%JSONFIELDNAME = "selftext", MAXLEN = 3000000); 11 | 12 | Property Author As %String(%JSONFIELDNAME = "author", MAXLEN = 255); 13 | 14 | Property Url As %String(%JSONFIELDNAME = "url", MAXLEN = 255); 15 | 16 | Property CreatedUTC As %Float(%JSONFIELDNAME = "created_utc"); 17 | 18 | Storage Default 19 | { 20 | 21 | 22 | %%CLASSNAME 23 | 24 | 25 | OriginalJSON 26 | 27 | 28 | selftext 29 | 30 | 31 | title 32 | 33 | 34 | author 35 | 36 | 37 | url 38 | 39 | 40 | created_utc 41 | 42 | 43 | Title 44 | 45 | 46 | Selftext 47 | 48 | 49 | Author 50 | 51 | 52 | Url 53 | 54 | 55 | CreatedUTC 56 | 57 | 58 | ^dc.Reddit.PostD 59 | PostDefaultData 60 | ^dc.Reddit.PostD 61 | ^dc.Reddit.PostI 62 | ^dc.Reddit.PostS 63 | %Storage.Persistent 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /demo/iris/PEX/WSGI.cls: -------------------------------------------------------------------------------- 1 | Class Demo.PEX.WSGI Extends Grongier.Service.WSGI 2 | { 3 | 4 | Parameter CLASSPATHS = "/irisdev/app/demo/python/reddit/"; 5 | 6 | Parameter MODULENAME = "app_flask"; 7 | 8 | Parameter APPNAME = "app"; 9 | 10 | Parameter DEVMODE = 0; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /demo/python/async/bo.py: -------------------------------------------------------------------------------- 1 | from grongier.pex import BusinessOperation 2 | from msg import MyMessage 3 | 4 | import time 5 | 6 | class MyBO(BusinessOperation): 7 | def on_message(self, request): 8 | print(f"Received message: {request.message}") 9 | time.sleep(1) 10 | return MyMessage(message=f"Hello, {request.message}") 11 | 12 | -------------------------------------------------------------------------------- /demo/python/async/bp.py: -------------------------------------------------------------------------------- 1 | from grongier.pex import BusinessProcess 2 | from msg import MyMessage 3 | 4 | 5 | class MyBP(BusinessProcess): 6 | 7 | def on_message(self, request): 8 | msg_one = MyMessage(message="Message1") 9 | msg_two = MyMessage(message="Message2") 10 | 11 | self.send_request_async("Python.MyBO", msg_one,completion_key="1") 12 | self.send_request_async("Python.MyBO", msg_two,completion_key="2") 13 | 14 | def on_response(self, request, response, call_request, call_response, completion_key): 15 | if completion_key == "1": 16 | self.response_one = call_response 17 | elif completion_key == "2": 18 | self.response_two = call_response 19 | 20 | def on_complete(self, request, response): 21 | self.log_info(f"Received response one: {self.response_one.message}") 22 | self.log_info(f"Received response two: {self.response_two.message}") 23 | 24 | 25 | -------------------------------------------------------------------------------- /demo/python/async/msg.py: -------------------------------------------------------------------------------- 1 | from grongier.pex import Message 2 | from dataclasses import dataclass 3 | 4 | @dataclass 5 | class MyMessage(Message): 6 | message : str = None -------------------------------------------------------------------------------- /demo/python/async/settings.py: -------------------------------------------------------------------------------- 1 | from bo import MyBO 2 | from bp import MyBP 3 | 4 | CLASSES = { 5 | "Python.MyBO": MyBO, 6 | "Python.MyBP": MyBP, 7 | } -------------------------------------------------------------------------------- /demo/python/async_ng/bo.py: -------------------------------------------------------------------------------- 1 | from iop import BusinessOperation 2 | from msg import MyMessage 3 | 4 | import time 5 | import random 6 | 7 | class MyAsyncNGBO(BusinessOperation): 8 | def on_message(self, request): 9 | print(f"Received message: {request.message}") 10 | rand = random.randint(1, 10) 11 | time.sleep(rand) 12 | return MyMessage(message=f"Hello, {request.message} after {rand} seconds") 13 | 14 | -------------------------------------------------------------------------------- /demo/python/async_ng/bp.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | 4 | from iop import BusinessProcess 5 | from msg import MyMessage 6 | 7 | 8 | class MyAsyncNGBP(BusinessProcess): 9 | 10 | def on_message(self, request): 11 | 12 | results = asyncio.run(self.await_response(request)) 13 | 14 | for result in results: 15 | self.logger.info(f"Received response: {result.message}") 16 | 17 | return MyMessage(message="All responses received") 18 | 19 | async def await_response(self, request): 20 | # create 1 to 10 messages 21 | tasks = [] 22 | for i in range(random.randint(1, 10)): 23 | tasks.append(self.send_request_async_ng("Python.MyAsyncNGBO", 24 | MyMessage(message=f"Message {i}"))) 25 | 26 | return await asyncio.gather(*tasks) 27 | 28 | -------------------------------------------------------------------------------- /demo/python/async_ng/msg.py: -------------------------------------------------------------------------------- 1 | from grongier.pex import Message 2 | from dataclasses import dataclass 3 | 4 | @dataclass 5 | class MyMessage(Message): 6 | message : str = None -------------------------------------------------------------------------------- /demo/python/async_ng/settings.py: -------------------------------------------------------------------------------- 1 | from bo import MyAsyncNGBO 2 | from bp import MyAsyncNGBP 3 | 4 | CLASSES = { 5 | "Python.MyAsyncNGBO": MyAsyncNGBO, 6 | "Python.MyAsyncNGBP": MyAsyncNGBP, 7 | } -------------------------------------------------------------------------------- /demo/python/bench/bo.py: -------------------------------------------------------------------------------- 1 | 2 | import iris 3 | from grongier.pex import BusinessOperation 4 | from msg import MyBenchPickle as msg 5 | 6 | class MyBench(BusinessOperation): 7 | 8 | def on_message(self, request): 9 | 10 | dump = request.property_name_0 11 | dump = request.property_name_1 12 | dump = request.property_name_2 13 | dump = request.property_name_3 14 | dump = request.property_name_4 15 | dump = request.property_name_5 16 | dump = request.property_name_6 17 | dump = request.property_name_7 18 | dump = request.property_name_8 19 | dump = request.property_name_9 20 | # self.log_info(len(request.bina)) 21 | 22 | 23 | return msg("test") -------------------------------------------------------------------------------- /demo/python/bench/bp.py: -------------------------------------------------------------------------------- 1 | from grongier.pex import BusinessProcess 2 | from msg import MyBenchPickle as message 3 | import iris 4 | 5 | import time 6 | 7 | class MyBench(BusinessProcess): 8 | 9 | def on_init(self): 10 | return 11 | 12 | def on_request(self, request): 13 | i = 0 14 | start = time.perf_counter() 15 | while i<1000: 16 | i=i+1 17 | msg = message('property_name_0','property_name_0','property_name_0','property_name_0','property_name_0','property_name_0','property_name_0','property_name_0','property_name_0','property_name_0') 18 | 19 | rsp = self.send_request_sync('Bench.bo.MyBench', msg) 20 | end = time.perf_counter() 21 | self.log_info(f"timed : {end-start}") 22 | return iris.cls('Ens.StringResponse')._New(f"{end-start}") 23 | 24 | if __name__ == '__main__': 25 | bp = MyBench() 26 | resp = bp._dispatch_on_request('', iris.cls('Ens.Request')._New()) 27 | resp -------------------------------------------------------------------------------- /demo/python/bench/msg.py: -------------------------------------------------------------------------------- 1 | from grongier.pex import Message 2 | from grongier.pex import PickleMessage 3 | from dataclasses import dataclass 4 | 5 | @dataclass 6 | class MyBench(Message): 7 | property_name_0:str = None 8 | property_name_1:str = None 9 | property_name_2:str = None 10 | property_name_3:str = None 11 | property_name_4:str = None 12 | property_name_5:str = None 13 | property_name_6:str = None 14 | property_name_7:str = None 15 | property_name_8:str = None 16 | property_name_9:str = None 17 | 18 | @dataclass 19 | class MyBenchPickle(PickleMessage): 20 | property_name_0:str = None 21 | property_name_1:str = None 22 | property_name_2:str = None 23 | property_name_3:str = None 24 | property_name_4:str = None 25 | property_name_5:str = None 26 | property_name_6:str = None 27 | property_name_7:str = None 28 | property_name_8:str = None 29 | property_name_9:str = None -------------------------------------------------------------------------------- /demo/python/duplex/adapter.py: -------------------------------------------------------------------------------- 1 | from grongier.pex import InboundAdapter 2 | 3 | class OperationAdapter(InboundAdapter): 4 | 5 | def on_task(self): 6 | self.log_info('on_task') 7 | self.business_host.OnProcessInput() -------------------------------------------------------------------------------- /demo/python/duplex/bo.py: -------------------------------------------------------------------------------- 1 | from grongier.pex import DuplexOperation 2 | import iris 3 | 4 | class Operation(DuplexOperation): 5 | 6 | def get_adapter_type(): 7 | """ 8 | Name of the registred Adapter 9 | """ 10 | return "Duplex.adapter.OperationAdapter" 11 | 12 | def on_message(self,input): 13 | self.log_info('on_message') 14 | 15 | def on_private_session_started(self,source_config_name,self_generated): 16 | self.log_info('In on_private_session_started') 17 | return 18 | 19 | def on_private_session_stopped(self,source_config_name,self_generated,message): 20 | self.log_info('In on_private_session_stopped') 21 | return 22 | 23 | if __name__ == '__main__': 24 | d = Operation() 25 | print('hello') 26 | print(d._get_info()) 27 | print('hello') -------------------------------------------------------------------------------- /demo/python/duplex/bp.py: -------------------------------------------------------------------------------- 1 | from grongier.pex import DuplexProcess 2 | 3 | class Process(DuplexProcess): 4 | 5 | def on_document(self,source_config_name,request): 6 | self.log_info('In on_document') 7 | return 8 | 9 | def on_private_session_started(self,source_config_name,self_generated): 10 | self.log_info('In on_private_session_started') 11 | return 12 | 13 | def on_private_session_stopped(self,source_config_name,self_generated,message): 14 | self.log_info('In on_private_session_started') 15 | return 16 | -------------------------------------------------------------------------------- /demo/python/duplex/bs.py: -------------------------------------------------------------------------------- 1 | from grongier.pex import DuplexService 2 | import iris 3 | 4 | class Service(DuplexService): 5 | 6 | _wait_for_next_call_interval = True 7 | 8 | def on_init(self): 9 | self._wait_for_next_call_interval = True 10 | return 11 | 12 | def get_adapter_type(): 13 | """ 14 | Name of the registred Adapter 15 | """ 16 | return "Ens.InboundAdapter" 17 | 18 | def on_process_input(self,input): 19 | self.send_document_to_process(iris.cls('Ens.Request')._New()) 20 | 21 | def on_private_session_started(self,source_config_name,self_generated): 22 | self.log_info('In on_private_session_started') 23 | return 24 | 25 | def on_private_session_stopped(self,source_config_name,self_generated,message): 26 | self.log_info('In on_private_session_stopped') 27 | return 28 | 29 | if __name__ == '__main__': 30 | d = Service() 31 | print('hello') 32 | print(d._get_info()) 33 | print('hello') -------------------------------------------------------------------------------- /demo/python/multi_sync/bo.py: -------------------------------------------------------------------------------- 1 | from iop import BusinessOperation 2 | from msg import MyMessage 3 | 4 | import time 5 | 6 | class MyMultiBO(BusinessOperation): 7 | def on_message(self, request): 8 | print(f"Received message: {request.message}") 9 | time.sleep(1) 10 | return MyMessage(message=f"Hello, {request.message}") 11 | 12 | -------------------------------------------------------------------------------- /demo/python/multi_sync/bp.py: -------------------------------------------------------------------------------- 1 | from iop import BusinessProcess 2 | from msg import MyMessage 3 | 4 | 5 | class MyMultiBP(BusinessProcess): 6 | 7 | def on_message(self, request): 8 | msg_one = MyMessage(message="Message1") 9 | msg_two = MyMessage(message="Message2") 10 | 11 | tuple_responses = self.send_multi_request_sync([("Python.MyMultiBO", msg_one), 12 | ("Python.MyMultiBO", msg_two)]) 13 | 14 | self.log_info("All requests have been processed") 15 | for target,request,response,status in tuple_responses: 16 | self.log_info(f"Received response: {response.message}") 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /demo/python/multi_sync/msg.py: -------------------------------------------------------------------------------- 1 | from iop import Message 2 | from dataclasses import dataclass 3 | 4 | @dataclass 5 | class MyMessage(Message): 6 | message : str = None -------------------------------------------------------------------------------- /demo/python/multi_sync/settings.py: -------------------------------------------------------------------------------- 1 | from bo import MyMultiBO 2 | from bp import MyMultiBP 3 | 4 | CLASSES = { 5 | "Python.MyMultiBO": MyMultiBO, 6 | "Python.MyMultiBP": MyMultiBP, 7 | } -------------------------------------------------------------------------------- /demo/python/reddit/app_flask.py: -------------------------------------------------------------------------------- 1 | # create a Flask app 2 | from flask import Flask, request 3 | 4 | app = Flask(__name__) 5 | 6 | @app.route('/') 7 | def hello_world(): 8 | """ 9 | Returns a greeting message. 10 | """ 11 | return 'Hello, World!' 12 | 13 | # add a route to say hello to any name 14 | @app.route('/name/') 15 | def hello_name(name): 16 | """ 17 | Returns a greeting message with a name. 18 | """ 19 | return f'Hello, {name}!' 20 | 21 | # add a post route 22 | @app.route('/', methods=['POST']) 23 | def post(): 24 | """ 25 | Returns a post message. 26 | """ 27 | payload = request.get_json() 28 | # return the payload 29 | return payload 30 | -------------------------------------------------------------------------------- /demo/python/reddit/bp.py: -------------------------------------------------------------------------------- 1 | from iop import BusinessProcess 2 | 3 | from message import PostMessage 4 | from obj import PostClass 5 | 6 | import iris 7 | 8 | class FilterPostRoutingRule(BusinessProcess): 9 | """ 10 | This process receive a PostMessage containing a reddit post. 11 | It then understand if the post is about a dog or a cat or nothing and 12 | fill the right infomation inside the PostMessage before sending it to 13 | the FileOperation operation. 14 | """ 15 | def on_init(self): 16 | 17 | if not hasattr(self,'target'): 18 | self.target = "Python.FileOperation" 19 | 20 | return 21 | 22 | def iris_to_python(self, request:'iris.dc.Demo.PostMessage'): 23 | 24 | request = PostMessage(post=PostClass(title=request.Post.Title, 25 | selftext=request.Post.Selftext, 26 | author=request.Post.Author, 27 | url=request.Post.Url, 28 | created_utc=request.Post.CreatedUTC, 29 | original_json=request.Post.OriginalJSON)) 30 | return self.on_python_message(request) 31 | 32 | def on_python_message(self, request: PostMessage): 33 | if 'dog'.upper() in request.post.selftext.upper(): 34 | request.to_email_address = 'dog@company.com' 35 | request.found = 'Dog' 36 | if 'cat'.upper() in request.post.selftext.upper(): 37 | request.to_email_address = 'cat@company.com' 38 | request.found = 'Cat' 39 | 40 | if request.found is not None: 41 | self.send_request_sync(self.target,request) 42 | return 43 | -------------------------------------------------------------------------------- /demo/python/reddit/message.py: -------------------------------------------------------------------------------- 1 | from grongier.pex import Message 2 | 3 | from dataclasses import dataclass 4 | 5 | from obj import PostClass 6 | 7 | @dataclass 8 | class PostMessage(Message): 9 | post:PostClass = None 10 | to_email_address:str = None 11 | found:str = None 12 | -------------------------------------------------------------------------------- /demo/python/reddit/obj.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from dataclasses_json import LetterCase, dataclass_json, config 3 | 4 | @dataclass_json(letter_case=LetterCase.CAMEL) 5 | @dataclass 6 | class PostClass: 7 | title: str 8 | selftext : str 9 | author: str 10 | url: str 11 | created_utc: float = field(metadata=config(field_name="created_utc")) 12 | original_json: str = None -------------------------------------------------------------------------------- /demo/python/requirements.txt: -------------------------------------------------------------------------------- 1 | dataclasses-json==0.5.7 2 | requests==2.32.2 3 | iris-pex-embedded-python>=3.0.0 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | services: 3 | iris: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | restart: always 8 | environment: 9 | - ISC_CPF_MERGE_FILE=/irisdev/app/merge.cpf 10 | ports: 11 | - 1972 12 | - 52795:52773 13 | - 53773 14 | command: 15 | - --check-caps false 16 | volumes: 17 | - ./:/irisdev/app 18 | - ./src/grongier:/usr/irissys/mgr/python/grongier 19 | - ./src/iop:/usr/irissys/mgr/python/iop 20 | entrypoint: ["sh", "/irisdev/app/entrypoint.sh"] -------------------------------------------------------------------------------- /dockerfile-ci: -------------------------------------------------------------------------------- 1 | ARG BASE=intersystemsdc/iris-community 2 | FROM $BASE 3 | 4 | COPY --chown=irisowner:irisowner . /irisdev/app 5 | 6 | WORKDIR /irisdev/app 7 | 8 | # map the source code of iop into iris python lib 9 | RUN ln -s /irisdev/app/src/iop /usr/irissys/mgr/python/iop 10 | # for retrocompatibility 11 | RUN ln -s /irisdev/app/src/grongier /usr/irissys/mgr/python/grongier 12 | 13 | ## Python stuff 14 | ENV IRISUSERNAME="SuperUser" 15 | ENV IRISPASSWORD="SYS" 16 | ENV IRISNAMESPACE="IRISAPP" 17 | ENV PYTHON_PATH=/usr/irissys/bin/ 18 | ENV LD_LIBRARY_PATH=${ISC_PACKAGE_INSTALLDIR}/bin 19 | ENV PATH="/home/irisowner/.local/bin:/usr/irissys/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/irisowner/bin" 20 | 21 | RUN pip install -r requirements-dev.txt 22 | 23 | ENTRYPOINT [ "/irisdev/app/test-in-docker.sh" ] -------------------------------------------------------------------------------- /docs/benchmarks.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | 8 senarios with thoses parameters : 4 | - 100 messages 5 | - body : simple string `test` 6 | 7 | | Scenario | Time (s) | 8 | | --- | --- | 9 | | Python BP to Python BO with Iris Message | 0.239 | 10 | | Python BP to Python BO with Python Message | 0.232 | 11 | | ObjetScript BP to Python BO with Iris Message | 0.294 | 12 | | ObjetScript BP to Python BO with Python Message | 0.242 | 13 | | Python BP to ObjetScript BO with Iris Message | 0.242 | 14 | | Python BP to ObjetScript BO with Python Message | 0.275 | 15 | | ObjetScript BP to ObjetScript BO with Iris Message | 0.159 | 16 | | ObjetScript BP to ObjetScript BO with Python Message | 0.182 | 17 | 18 | Benchmarked can be run in the unit test with the following command : 19 | 20 | ```bash 21 | pytest src/tests/test_bench.py 22 | ``` -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | --8<-- "CHANGELOG.md" -------------------------------------------------------------------------------- /docs/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Code of conduct 2 | 3 | TODO -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Run the unit tests 4 | 5 | To run the unit tests, you must follow the steps below: 6 | 7 | ### Create a virtual environment. 8 | 9 | ```bash 10 | python -m venv .venv 11 | source .venv/bin/activate 12 | ``` 13 | 14 | ### Install the dependencies. 15 | 16 | ```bash 17 | pip install -r requirements-dev.txt 18 | ``` 19 | 20 | ### Have a running IRIS instance. 21 | 22 | Here you can choose between: 23 | 24 | - Local installation of IRIS 25 | - Docker installation of IRIS 26 | 27 | #### Local installation of IRIS 28 | 29 | ##### Install IRIS locally. 30 | 31 | - [Local installation of IRIS](https://docs.intersystems.com/irislatest/csp/docbook/Doc.View.cls?KEY=PAGE_deployment_install) 32 | - [Python interpreter compatible with the version of IRIS](https://docs.intersystems.com/iris20243/csp/docbook/Doc.View.cls?KEY=GEPYTHON_prereqs#GEPYTHON_prereqs_version) 33 | - [Iris embedded python wrapper](https://github.com/grongierisc/iris-embedded-python-wrapper) 34 | - Make sure to follow the [instructions to install the wrapper in your IRIS instance.](https://github.com/grongierisc/iris-embedded-python-wrapper?tab=readme-ov-file#pre-requisites) 35 | 36 | 37 | ##### Set up the IRIS instance. 38 | 39 | Then, symbolically this git to the IRIS pyhton directory: 40 | 41 | ```bash 42 | ln -s /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 | [![PyPI - Status](https://img.shields.io/pypi/status/iris-pex-embedded-python)](https://pypi.org/project/iris-pex-embedded-python/) 4 | [![PyPI](https://img.shields.io/pypi/v/iris-pex-embedded-python)](https://pypi.org/project/iris-pex-embedded-python/) 5 | [![PyPI - Downloads](https://img.shields.io/pypi/dm/iris-pex-embedded-python)](https://pypi.org/project/iris-pex-embedded-python/) 6 | [![PyPI - License](https://img.shields.io/pypi/l/iris-pex-embedded-python)](https://pypi.org/project/iris-pex-embedded-python/) 7 | ![GitHub last commit](https://img.shields.io/github/last-commit/grongierisc/interoperability-embedded-python) 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 | ![Settings in production](img/settings-in-production.png) 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 | ![Custom settings](img/custom_settings.png) 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 --------------------------------------------------------------------------------