From 60f75ab41e3886e8826cb7468d51302a73d5954a Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:05:32 +0100 Subject: [PATCH 01/31] fixxes Docs Test --- .github/workflows/docs.yml | 20 +++++--------------- .github/workflows/python-package.yml | 2 +- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 28672b6..79ddefe 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -11,51 +11,41 @@ permissions: jobs: build: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: - python-version: "3.12" + python-version: "3.14" - # Install dependencies from your package directory - name: Install package run: | python -m pip install --upgrade pip pip install -e ./app/iLibrary - # Install documentation tool - - name: Install pdoc + - name: Install documentation tool run: pip install pdoc - # Build documentation - name: Build documentation run: | - python -m pip install --upgrade pip - pip install -e ./app/iLibrary - pip install pdoc pdoc app.iLibrary.src -o docs touch docs/.nojekyll - - uses: actions/upload-pages-artifact@v3 + - uses: actions/upload-pages-artifact@v4 with: path: docs/ deploy: needs: build runs-on: ubuntu-latest - permissions: pages: write id-token: write - environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} - steps: - id: deployment uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9ded05a..a644e67 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] # 1. Inject SECRETS as Environment Variables for the entire job env: From 2050e89c9fb599738616329ca79dc1f846deb9b2 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:06:09 +0100 Subject: [PATCH 02/31] fixxes Docs Test --- .github/workflows/docs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 79ddefe..49568f5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - Developer permissions: contents: read From 4a735c8fc2987fb8762bf0e6216ccee6b7790760 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:07:21 +0100 Subject: [PATCH 03/31] fixxes Docs Test --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 49568f5..1296a3e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -24,7 +24,7 @@ jobs: - name: Install package run: | python -m pip install --upgrade pip - pip install -e ./app/iLibrary + pip install -e . - name: Install documentation tool run: pip install pdoc From db56f98adbe4e0e735009603fb39586de28f5529 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:09:34 +0100 Subject: [PATCH 04/31] fixxes Docs Test --- .github/workflows/docs.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1296a3e..49d09d0 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -31,7 +31,10 @@ jobs: - name: Build documentation run: | - pdoc app.iLibrary.src -o docs + # Add your src folder to PYTHONPATH so Python can find the module + export PYTHONPATH=$(pwd)/app/iLibrary/src + # Use the actual module name inside src + pdoc ilibrary -o docs touch docs/.nojekyll - uses: actions/upload-pages-artifact@v4 From a6540632d2f1d82b362a2eda95d776e8bd1676d4 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:10:55 +0100 Subject: [PATCH 05/31] fixxes Docs Test --- .github/workflows/docs.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 49d09d0..16d9e5d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -31,10 +31,8 @@ jobs: - name: Build documentation run: | - # Add your src folder to PYTHONPATH so Python can find the module - export PYTHONPATH=$(pwd)/app/iLibrary/src - # Use the actual module name inside src - pdoc ilibrary -o docs + pip install pdoc + pdoc ./app/iLibrary/src/ilibrary --output-dir docs --force touch docs/.nojekyll - uses: actions/upload-pages-artifact@v4 From 2e12624a590927e69e58e3d65912a97c2782d996 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:11:38 +0100 Subject: [PATCH 06/31] fixxes Docs Test --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 16d9e5d..4e0ced9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -32,7 +32,7 @@ jobs: - name: Build documentation run: | pip install pdoc - pdoc ./app/iLibrary/src/ilibrary --output-dir docs --force + pdoc ./app/iLibrary/src/ilibrary -o docs touch docs/.nojekyll - uses: actions/upload-pages-artifact@v4 From fd8768d985a0b81a43d6f0343b8d142c7e37e0a5 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:17:45 +0100 Subject: [PATCH 07/31] fixxes Docs Test --- setup.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 3fb7d7f..79475ef 100644 --- a/setup.py +++ b/setup.py @@ -25,9 +25,9 @@ package_dir = {"": "app"}, packages = find_packages(where="app"), python_requires = ">=3.6", - # install_requires=[ - # "requests>=2.25.0", - # "pandas>=1.2.0", - # "pyodbc", # Often used for IBM i connectivity - # ], + install_requires=[ + "paramiko", + "pyodbc", + "python-dotenv", + ], ) \ No newline at end of file From 5aee43a75bddf3ce061799fe6401ee9e78e8aa63 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:20:20 +0100 Subject: [PATCH 08/31] fixxes Docs Test --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 79475ef..812b7d4 100644 --- a/setup.py +++ b/setup.py @@ -22,8 +22,8 @@ "Development Status :: 4 - Beta", "Operating System :: OS Independent", ], - package_dir = {"": "app"}, - packages = find_packages(where="app"), + package_dir={"": "app/iLibrary/src"}, + packages=find_packages(where="app/iLibrary/src"), python_requires = ">=3.6", install_requires=[ "paramiko", From 695f82073411eae01d3121d8540eb6575b53d829 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:22:06 +0100 Subject: [PATCH 09/31] fixxes Docs Test --- .github/workflows/docs.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4e0ced9..75e6f4a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -21,18 +21,15 @@ jobs: with: python-version: "3.14" - - name: Install package + - name: Install package and dependencies run: | python -m pip install --upgrade pip pip install -e . - - name: Install documentation tool - run: pip install pdoc - - name: Build documentation run: | pip install pdoc - pdoc ./app/iLibrary/src/ilibrary -o docs + pdoc ilibrary -o docs touch docs/.nojekyll - uses: actions/upload-pages-artifact@v4 From 6839bb4af9a7baa5c75df2733b9d333169145cb5 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:24:13 +0100 Subject: [PATCH 10/31] fixxes Docs Test --- .github/workflows/docs.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 75e6f4a..2604705 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -21,15 +21,17 @@ jobs: with: python-version: "3.14" - - name: Install package and dependencies + - name: Install package run: | python -m pip install --upgrade pip pip install -e . + - name: Install documentation tool + run: pip install pdoc + - name: Build documentation run: | - pip install pdoc - pdoc ilibrary -o docs + PYTHONPATH=app/iLibrary/src pdoc ilibrary -o docs touch docs/.nojekyll - uses: actions/upload-pages-artifact@v4 From b8c680a82fb1ad637746c3347cebf87a97635e0a Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:25:44 +0100 Subject: [PATCH 11/31] fixxes Docs Test --- .github/workflows/docs.yml | 44 ++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2604705..9763edb 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,4 +1,4 @@ -name: website +name: Documentation on: push: @@ -8,45 +8,61 @@ on: permissions: contents: read + pages: write + id-token: write jobs: build: runs-on: ubuntu-latest + steps: - uses: actions/checkout@v6 - with: - persist-credentials: false + # ----------------------------- + # Setup Python + # ----------------------------- - uses: actions/setup-python@v6 with: python-version: "3.14" - - name: Install package + # ----------------------------- + # Install package + dependencies + # ----------------------------- + - name: Install project run: | python -m pip install --upgrade pip pip install -e . - - name: Install documentation tool + # ----------------------------- + # Install documentation tool + # ----------------------------- + - name: Install pdoc run: pip install pdoc + # ----------------------------- + # Build documentation + # IMPORTANT: + # src layout → use PYTHONPATH + # ----------------------------- - name: Build documentation run: | + mkdir -p docs PYTHONPATH=app/iLibrary/src pdoc ilibrary -o docs touch docs/.nojekyll - - uses: actions/upload-pages-artifact@v4 + # ----------------------------- + # Upload artifact for GitHub Pages + # ----------------------------- + - uses: actions/upload-pages-artifact@v3 with: path: docs/ + # ----------------------------- + # Deploy to GitHub Pages + # ----------------------------- deploy: needs: build runs-on: ubuntu-latest - permissions: - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} + steps: - - id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + - uses: actions/deploy-pages@v4 \ No newline at end of file From 293b11baa07bc7e48d60c78234811faca8579504 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:27:49 +0100 Subject: [PATCH 12/31] fixxes Docs Test --- .github/workflows/docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9763edb..49c1ad5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -46,8 +46,8 @@ jobs: # ----------------------------- - name: Build documentation run: | - mkdir -p docs - PYTHONPATH=app/iLibrary/src pdoc ilibrary -o docs + pip install pdoc + PYTHONPATH=app/iLibrary/src pdoc ilibrary --output-dir docs touch docs/.nojekyll # ----------------------------- From 07e2d0ed9061f261f87e2bdd9878e3afb1fcc53b Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:35:19 +0100 Subject: [PATCH 13/31] fixxes Docs Test --- .github/workflows/docs.yml | 69 ++++++++++++++------------------------ docs/makedoc.py | 26 ++++++++++++++ 2 files changed, 52 insertions(+), 43 deletions(-) create mode 100644 docs/makedoc.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 49c1ad5..0b65655 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,68 +1,51 @@ -name: Documentation +name: website +# build the documentation whenever there are new commits on main on: push: branches: - main - - Developer + # Alternative: only build for tags. + # tags: + # - '*' +# security: restrict permissions for CI jobs. permissions: contents: read - pages: write - id-token: write jobs: + # Build the documentation and upload the static HTML files as an artifact. build: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v6 - - # ----------------------------- - # Setup Python - # ----------------------------- + with: + persist-credentials: false - uses: actions/setup-python@v6 with: - python-version: "3.14" + python-version: '3.14' - # ----------------------------- - # Install package + dependencies - # ----------------------------- - - name: Install project - run: | - python -m pip install --upgrade pip - pip install -e . + # ADJUST THIS: install all dependencies (including pdoc) + - run: pip install -e . + # ADJUST THIS: build your documentation into docs/. + # We use a custom build script for pdoc itself, ideally you just run `pdoc -o docs/ ...` here. + - run: python docs/make.py - # ----------------------------- - # Install documentation tool - # ----------------------------- - - name: Install pdoc - run: pip install pdoc - - # ----------------------------- - # Build documentation - # IMPORTANT: - # src layout → use PYTHONPATH - # ----------------------------- - - name: Build documentation - run: | - pip install pdoc - PYTHONPATH=app/iLibrary/src pdoc ilibrary --output-dir docs - touch docs/.nojekyll - - # ----------------------------- - # Upload artifact for GitHub Pages - # ----------------------------- - - uses: actions/upload-pages-artifact@v3 + - uses: actions/upload-pages-artifact@v4 with: path: docs/ - # ----------------------------- - # Deploy to GitHub Pages - # ----------------------------- + # Deploy the artifact to GitHub pages. + # This is a separate job so that only actions/deploy-pages has the necessary permissions. deploy: needs: build runs-on: ubuntu-latest - + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} steps: - - uses: actions/deploy-pages@v4 \ No newline at end of file + - id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/docs/makedoc.py b/docs/makedoc.py new file mode 100644 index 0000000..f7429c3 --- /dev/null +++ b/docs/makedoc.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +from pathlib import Path +import shutil +from pdoc import pdoc, render + +# 1. Define your paths +here = Path(__file__).parent +# Change "api" to whatever folder name you want in MkDocs +out = here / "docs" / "reference" +# Change "my_package" to the name of your folder containing .py files +src_folder = "../app/iLibrary/src" + +if out.exists(): + shutil.rmtree(out) + +# 2. Configure (Optional) +# If you don't have a 'pdoc-template' folder, comment this line out +# render.configure(template_directory=here / "pdoc-template") + +# 3. Generate for YOUR project +# Replace "your_project_name" with your actual package/module name +pdoc(src_folder, output_directory=out) + +# # 4. Rename for MkDocs +# for f in out.glob("**/*.html"): +# f.rename(f.with_suffix(".md")) \ No newline at end of file From 185ccb73ae441b6bcb8e34f6bcf57b48be55905a Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:35:44 +0100 Subject: [PATCH 14/31] fixxes Docs Test --- .github/workflows/docs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0b65655..73dc281 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,6 +5,7 @@ on: push: branches: - main + - developer # Alternative: only build for tags. # tags: # - '*' From 217d3153041b15e0136e75e7e6fdc4b2e0df27e8 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:36:28 +0100 Subject: [PATCH 15/31] fixxes Docs Test --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 73dc281..10e83d8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,7 +5,7 @@ on: push: branches: - main - - developer + - Developer # Alternative: only build for tags. # tags: # - '*' From a8ef9b5ac503e5afeb0a561d8302511e543a75ef Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:37:05 +0100 Subject: [PATCH 16/31] fixxes Docs Test --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 10e83d8..2e839ab 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -30,7 +30,7 @@ jobs: - run: pip install -e . # ADJUST THIS: build your documentation into docs/. # We use a custom build script for pdoc itself, ideally you just run `pdoc -o docs/ ...` here. - - run: python docs/make.py + - run: python docs/makedoc.py - uses: actions/upload-pages-artifact@v4 with: From bad0a6a87e712a73be1aa976108bba687bd23afa Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:37:56 +0100 Subject: [PATCH 17/31] fixxes Docs Test --- .github/workflows/docs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2e839ab..95614e7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,6 +28,7 @@ jobs: # ADJUST THIS: install all dependencies (including pdoc) - run: pip install -e . + - run: pip install pdoc # ADJUST THIS: build your documentation into docs/. # We use a custom build script for pdoc itself, ideally you just run `pdoc -o docs/ ...` here. - run: python docs/makedoc.py From 6d7faf1058acf34db985a7ace2f05013dc789d86 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:41:03 +0100 Subject: [PATCH 18/31] fixxes Docs Test --- docs/makedoc.py | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/docs/makedoc.py b/docs/makedoc.py index f7429c3..927c1b4 100644 --- a/docs/makedoc.py +++ b/docs/makedoc.py @@ -1,26 +1,37 @@ #!/usr/bin/env python3 from pathlib import Path import shutil -from pdoc import pdoc, render +from pdoc import pdoc -# 1. Define your paths -here = Path(__file__).parent -# Change "api" to whatever folder name you want in MkDocs -out = here / "docs" / "reference" -# Change "my_package" to the name of your folder containing .py files -src_folder = "../app/iLibrary/src" +# 1. Define paths relative to this script +# .parent points to the folder containing this script (e.g., your 'docs_scripts' folder) +script_dir = Path(__file__).parent.resolve() -if out.exists(): - shutil.rmtree(out) +# Define the root of your GitHub repo (one level up from the script folder) +root_path = script_dir.parent -# 2. Configure (Optional) -# If you don't have a 'pdoc-template' folder, comment this line out -# render.configure(template_directory=here / "pdoc-template") +# Path to your source code: root/src +src_folder = root_path / "app" / "iLibrary" /"src" -# 3. Generate for YOUR project -# Replace "your_project_name" with your actual package/module name -pdoc(src_folder, output_directory=out) +# Path to the output: root/docs/reference +out = root_path / "docs" / "reference" -# # 4. Rename for MkDocs -# for f in out.glob("**/*.html"): -# f.rename(f.with_suffix(".md")) \ No newline at end of file +def generate_docs(): + # Clean up old documentation + if out.exists(): + print(f"Cleaning up old docs at: {out}") + shutil.rmtree(out) + + print(f"Generating docs from: {src_folder}") + print(f"Outputting to: {out}") + + # 2. Generate for your project + # pdoc will crawl the src_folder and generate HTML by default + pdoc(src_folder, output_directory=out) + + # 3. Optional: Rename for MkDocs if you are using the MkDocs-Material logic + # for f in out.glob("**/*.html"): + # f.rename(f.with_suffix(".md")) + +if __name__ == "__main__": + generate_docs() \ No newline at end of file From 510320c9c3231a343a0584d0a5a8b02c9075a124 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:49:11 +0100 Subject: [PATCH 19/31] fixxes Docs Test --- .github/workflows/docs.yml | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 95614e7..224422a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,44 +1,43 @@ name: website -# build the documentation whenever there are new commits on main on: push: branches: - main - Developer - # Alternative: only build for tags. - # tags: - # - '*' -# security: restrict permissions for CI jobs. permissions: contents: read jobs: - # Build the documentation and upload the static HTML files as an artifact. build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: persist-credentials: false + - uses: actions/setup-python@v6 with: - python-version: '3.14' + python-version: '3.12' + + - name: Install dependencies + run: | + pip install -e . + pip install pdoc mkdocs-material + # Note: mkdocs-material is the standard theme, add any other plugins you use here. + + - name: Generate API Reference + run: python docs/makedoc.py - # ADJUST THIS: install all dependencies (including pdoc) - - run: pip install -e . - - run: pip install pdoc - # ADJUST THIS: build your documentation into docs/. - # We use a custom build script for pdoc itself, ideally you just run `pdoc -o docs/ ...` here. - - run: python docs/makedoc.py + - name: Build Static Website + run: mkdocs build - - uses: actions/upload-pages-artifact@v4 + - name: Upload Page Artifact + uses: actions/upload-pages-artifact@v4 with: - path: docs/ + path: site/ - # Deploy the artifact to GitHub pages. - # This is a separate job so that only actions/deploy-pages has the necessary permissions. deploy: needs: build runs-on: ubuntu-latest From 7e6e06e5ec20e3e567c69f27fc06c5a468db581f Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:52:37 +0100 Subject: [PATCH 20/31] fixxes Docs Test --- docs/makedoc.py | 4 +- docs/reference/iLibrary/src.md | 242 +++ docs/reference/iLibrary/src/Library.md | 563 +++++++ docs/reference/iLibrary/src/User.md | 556 +++++++ .../iLibrary/src/getInfoForLibrary.md | 604 +++++++ .../iLibrary/src/getUserInfoForUser.md | 659 ++++++++ docs/reference/iLibrary/src/saveLibrary.md | 1466 +++++++++++++++++ docs/reference/iLibrary/src/sendMSG.md | 497 ++++++ docs/reference/index.md | 7 + docs/reference/search.js | 46 + 10 files changed, 4643 insertions(+), 1 deletion(-) create mode 100644 docs/reference/iLibrary/src.md create mode 100644 docs/reference/iLibrary/src/Library.md create mode 100644 docs/reference/iLibrary/src/User.md create mode 100644 docs/reference/iLibrary/src/getInfoForLibrary.md create mode 100644 docs/reference/iLibrary/src/getUserInfoForUser.md create mode 100644 docs/reference/iLibrary/src/saveLibrary.md create mode 100644 docs/reference/iLibrary/src/sendMSG.md create mode 100644 docs/reference/index.md create mode 100644 docs/reference/search.js diff --git a/docs/makedoc.py b/docs/makedoc.py index 927c1b4..9fc69b7 100644 --- a/docs/makedoc.py +++ b/docs/makedoc.py @@ -32,6 +32,8 @@ def generate_docs(): # 3. Optional: Rename for MkDocs if you are using the MkDocs-Material logic # for f in out.glob("**/*.html"): # f.rename(f.with_suffix(".md")) - + # Rename .html to .md + for f in out.glob("**/*.html"): + f.rename(f.with_suffix(".md")) if __name__ == "__main__": generate_docs() \ No newline at end of file diff --git a/docs/reference/iLibrary/src.md b/docs/reference/iLibrary/src.md new file mode 100644 index 0000000..e98eb43 --- /dev/null +++ b/docs/reference/iLibrary/src.md @@ -0,0 +1,242 @@ + + + + + + + iLibrary.src API documentation + + + + + + + + + +
+
+

+iLibrary.src

+ + + + + + +
1
+
+ + +
+
+ + \ No newline at end of file diff --git a/docs/reference/iLibrary/src/Library.md b/docs/reference/iLibrary/src/Library.md new file mode 100644 index 0000000..258cebe --- /dev/null +++ b/docs/reference/iLibrary/src/Library.md @@ -0,0 +1,563 @@ + + + + + + + iLibrary.src.Library API documentation + + + + + + + + + +
+
+

+iLibrary.src.Library

+ + + + + + +
 1from os.path import join
+ 2import paramiko
+ 3import pyodbc
+ 4import json
+ 5from datetime import datetime, date
+ 6from decimal import Decimal
+ 7from .getInfoForLibrary import *
+ 8from .saveLibrary import *
+ 9
+10
+11
+12class Library(getInfoForLibrary, saveLibrary):
+13    """
+14    A class to manage libraries and files on an IBM i system.
+15
+16    It provides methods to connect to the system via pyodbc for SQL and
+17    paramiko for SFTP transfers.
+18    """
+19
+20    # ------------------------------------------------------
+21    # __init__ - initzialise the class
+22    # ------------------------------------------------------
+23    def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str):
+24        """
+25        Initializes the class attributes for a database connection.
+26        The actual connection is established in the __enter__ method.
+27
+28        Args:
+29            db_user (str): The user ID for the database connection.
+30            db_password (str): The password for the database user.
+31            db_host (str): The system/host name for the database connection.
+32            db_driver (str): The ODBC driver to be used.
+33        """
+34        self.db_user = db_user
+35        self.db_host = db_host
+36        self.db_driver = db_driver
+37        self.db_password = db_password
+38
+39    # ------------------------------------------------------
+40    # __enter__ - enter to the class
+41    # ------------------------------------------------------
+42    def __enter__(self) -> 'Library':
+43        """
+44        Establishes the database connection when entering a 'with' block.
+45        """
+46        try:
+47            conn_str = (
+48                f"DRIVER={self.db_driver};"
+49                f"SYSTEM={self.db_host};"
+50                f"UID={self.db_user};"
+51                f"PWD={self.db_password};"
+52            )
+53            self.conn = pyodbc.connect(conn_str, autocommit=True)
+54            return self
+55        except pyodbc.Error as ex:
+56            sqlstate = ex.args[0]
+57            print(f"Database connection failed with error: {sqlstate}")
+58            raise
+59
+60    # ------------------------------------------------------
+61    # __exit__ - leave the class
+62    # ------------------------------------------------------
+63    def __exit__(self, exc_type, exc_val, exc_tb):
+64        """
+65        Closes the database connection when exiting a 'with' block.
+66        This method is called automatically, even if an error occurred.
+67        """
+68        self.iclose()
+69
+70
+71    # ------------------------------------------------------
+72    # iClose - close connection
+73    # ------------------------------------------------------
+74    def iclose(self):
+75        """
+76        A helper method to close the connection, also useful for manual closure.
+77        """
+78        if self.conn and not self.conn.closed:
+79            self.conn.close()
+80            pass
+
+ + +
+
+ + + +
13class Library(getInfoForLibrary, saveLibrary):
+14    """
+15    A class to manage libraries and files on an IBM i system.
+16
+17    It provides methods to connect to the system via pyodbc for SQL and
+18    paramiko for SFTP transfers.
+19    """
+20
+21    # ------------------------------------------------------
+22    # __init__ - initzialise the class
+23    # ------------------------------------------------------
+24    def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str):
+25        """
+26        Initializes the class attributes for a database connection.
+27        The actual connection is established in the __enter__ method.
+28
+29        Args:
+30            db_user (str): The user ID for the database connection.
+31            db_password (str): The password for the database user.
+32            db_host (str): The system/host name for the database connection.
+33            db_driver (str): The ODBC driver to be used.
+34        """
+35        self.db_user = db_user
+36        self.db_host = db_host
+37        self.db_driver = db_driver
+38        self.db_password = db_password
+39
+40    # ------------------------------------------------------
+41    # __enter__ - enter to the class
+42    # ------------------------------------------------------
+43    def __enter__(self) -> 'Library':
+44        """
+45        Establishes the database connection when entering a 'with' block.
+46        """
+47        try:
+48            conn_str = (
+49                f"DRIVER={self.db_driver};"
+50                f"SYSTEM={self.db_host};"
+51                f"UID={self.db_user};"
+52                f"PWD={self.db_password};"
+53            )
+54            self.conn = pyodbc.connect(conn_str, autocommit=True)
+55            return self
+56        except pyodbc.Error as ex:
+57            sqlstate = ex.args[0]
+58            print(f"Database connection failed with error: {sqlstate}")
+59            raise
+60
+61    # ------------------------------------------------------
+62    # __exit__ - leave the class
+63    # ------------------------------------------------------
+64    def __exit__(self, exc_type, exc_val, exc_tb):
+65        """
+66        Closes the database connection when exiting a 'with' block.
+67        This method is called automatically, even if an error occurred.
+68        """
+69        self.iclose()
+70
+71
+72    # ------------------------------------------------------
+73    # iClose - close connection
+74    # ------------------------------------------------------
+75    def iclose(self):
+76        """
+77        A helper method to close the connection, also useful for manual closure.
+78        """
+79        if self.conn and not self.conn.closed:
+80            self.conn.close()
+81            pass
+
+ + +

A class to manage libraries and files on an IBM i system.

+ +

It provides methods to connect to the system via pyodbc for SQL and +paramiko for SFTP transfers.

+
+ + +
+ +
+ + Library(db_user: str, db_password: str, db_host: str, db_driver: str) + + + +
+ +
24    def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str):
+25        """
+26        Initializes the class attributes for a database connection.
+27        The actual connection is established in the __enter__ method.
+28
+29        Args:
+30            db_user (str): The user ID for the database connection.
+31            db_password (str): The password for the database user.
+32            db_host (str): The system/host name for the database connection.
+33            db_driver (str): The ODBC driver to be used.
+34        """
+35        self.db_user = db_user
+36        self.db_host = db_host
+37        self.db_driver = db_driver
+38        self.db_password = db_password
+
+ + +

Initializes the class attributes for a database connection. +The actual connection is established in the __enter__ method.

+ +

Args: + db_user (str): The user ID for the database connection. + db_password (str): The password for the database user. + db_host (str): The system/host name for the database connection. + db_driver (str): The ODBC driver to be used.

+
+ + +
+
+
+ db_user + + +
+ + + + +
+
+
+ db_host + + +
+ + + + +
+
+
+ db_driver + + +
+ + + + +
+
+
+ db_password + + +
+ + + + +
+
+ +
+ + def + iclose(self): + + + +
+ +
75    def iclose(self):
+76        """
+77        A helper method to close the connection, also useful for manual closure.
+78        """
+79        if self.conn and not self.conn.closed:
+80            self.conn.close()
+81            pass
+
+ + +

A helper method to close the connection, also useful for manual closure.

+
+ + +
+ +
+
+ + \ No newline at end of file diff --git a/docs/reference/iLibrary/src/User.md b/docs/reference/iLibrary/src/User.md new file mode 100644 index 0000000..b90738e --- /dev/null +++ b/docs/reference/iLibrary/src/User.md @@ -0,0 +1,556 @@ + + + + + + + iLibrary.src.User API documentation + + + + + + + + + +
+
+

+iLibrary.src.User

+ + + + + + +
 1from os.path import join
+ 2import paramiko
+ 3import pyodbc
+ 4import json
+ 5from datetime import datetime, date
+ 6from decimal import Decimal
+ 7from .getUserInfoForUser import *
+ 8from .sendMSG import *
+ 9
+10class User(getUserInfoForUser, sendMSG):
+11    """
+12        A class to manage User on IBMi System
+13
+14        It provides methods to connect to the system via pyodbc for SQL and
+15        paramiko for SFTP transfers.
+16    """
+17
+18    # ------------------------------------------------------
+19    # __init__ - initzialise the class
+20    # ------------------------------------------------------
+21    def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str):
+22        """
+23        Initializes the class attributes for a database connection.
+24        The actual connection is established in the __enter__ method.
+25
+26        Args:
+27            db_user (str): The user ID for the database connection.
+28            db_password (str): The password for the database user.
+29            db_host (str): The system/host name for the database connection.
+30            db_driver (str): The ODBC driver to be used.
+31        """
+32        self.db_user = db_user
+33        self.db_host = db_host
+34        self.db_driver = db_driver
+35        self.db_password = db_password
+36
+37    # ------------------------------------------------------
+38    # __enter__ - enter to the class
+39    # ------------------------------------------------------
+40    def __enter__(self) -> 'User':
+41        """
+42        Establishes the database connection when entering a 'with' block.
+43        """
+44        try:
+45            conn_str = (
+46                f"DRIVER={self.db_driver};"
+47                f"SYSTEM={self.db_host};"
+48                f"UID={self.db_user};"
+49                f"PWD={self.db_password};"
+50            )
+51            self.conn = pyodbc.connect(conn_str, autocommit=True)
+52            return self
+53        except pyodbc.Error as ex:
+54            sqlstate = ex.args[0]
+55            print(f"Database connection failed with error: {sqlstate}")
+56            raise
+57
+58    # ------------------------------------------------------
+59    # __exit__ - leave the class
+60    # ------------------------------------------------------
+61    def __exit__(self, exc_type, exc_val, exc_tb):
+62        """
+63        Closes the database connection when exiting a 'with' block.
+64        This method is called automatically, even if an error occurred.
+65        """
+66        self.iclose()
+67
+68    # ------------------------------------------------------
+69    # iClose - close connection
+70    # ------------------------------------------------------
+71    def iclose(self):
+72        """
+73        A helper method to close the connection, also useful for manual closure.
+74        """
+75        if self.conn and not self.conn.closed:
+76            self.conn.close()
+77            pass
+
+ + +
+
+ + + +
11class User(getUserInfoForUser, sendMSG):
+12    """
+13        A class to manage User on IBMi System
+14
+15        It provides methods to connect to the system via pyodbc for SQL and
+16        paramiko for SFTP transfers.
+17    """
+18
+19    # ------------------------------------------------------
+20    # __init__ - initzialise the class
+21    # ------------------------------------------------------
+22    def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str):
+23        """
+24        Initializes the class attributes for a database connection.
+25        The actual connection is established in the __enter__ method.
+26
+27        Args:
+28            db_user (str): The user ID for the database connection.
+29            db_password (str): The password for the database user.
+30            db_host (str): The system/host name for the database connection.
+31            db_driver (str): The ODBC driver to be used.
+32        """
+33        self.db_user = db_user
+34        self.db_host = db_host
+35        self.db_driver = db_driver
+36        self.db_password = db_password
+37
+38    # ------------------------------------------------------
+39    # __enter__ - enter to the class
+40    # ------------------------------------------------------
+41    def __enter__(self) -> 'User':
+42        """
+43        Establishes the database connection when entering a 'with' block.
+44        """
+45        try:
+46            conn_str = (
+47                f"DRIVER={self.db_driver};"
+48                f"SYSTEM={self.db_host};"
+49                f"UID={self.db_user};"
+50                f"PWD={self.db_password};"
+51            )
+52            self.conn = pyodbc.connect(conn_str, autocommit=True)
+53            return self
+54        except pyodbc.Error as ex:
+55            sqlstate = ex.args[0]
+56            print(f"Database connection failed with error: {sqlstate}")
+57            raise
+58
+59    # ------------------------------------------------------
+60    # __exit__ - leave the class
+61    # ------------------------------------------------------
+62    def __exit__(self, exc_type, exc_val, exc_tb):
+63        """
+64        Closes the database connection when exiting a 'with' block.
+65        This method is called automatically, even if an error occurred.
+66        """
+67        self.iclose()
+68
+69    # ------------------------------------------------------
+70    # iClose - close connection
+71    # ------------------------------------------------------
+72    def iclose(self):
+73        """
+74        A helper method to close the connection, also useful for manual closure.
+75        """
+76        if self.conn and not self.conn.closed:
+77            self.conn.close()
+78            pass
+
+ + +

A class to manage User on IBMi System

+ +

It provides methods to connect to the system via pyodbc for SQL and +paramiko for SFTP transfers.

+
+ + +
+ +
+ + User(db_user: str, db_password: str, db_host: str, db_driver: str) + + + +
+ +
22    def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str):
+23        """
+24        Initializes the class attributes for a database connection.
+25        The actual connection is established in the __enter__ method.
+26
+27        Args:
+28            db_user (str): The user ID for the database connection.
+29            db_password (str): The password for the database user.
+30            db_host (str): The system/host name for the database connection.
+31            db_driver (str): The ODBC driver to be used.
+32        """
+33        self.db_user = db_user
+34        self.db_host = db_host
+35        self.db_driver = db_driver
+36        self.db_password = db_password
+
+ + +

Initializes the class attributes for a database connection. +The actual connection is established in the __enter__ method.

+ +

Args: + db_user (str): The user ID for the database connection. + db_password (str): The password for the database user. + db_host (str): The system/host name for the database connection. + db_driver (str): The ODBC driver to be used.

+
+ + +
+
+
+ db_user + + +
+ + + + +
+
+
+ db_host + + +
+ + + + +
+
+
+ db_driver + + +
+ + + + +
+
+
+ db_password + + +
+ + + + +
+
+ +
+ + def + iclose(self): + + + +
+ +
72    def iclose(self):
+73        """
+74        A helper method to close the connection, also useful for manual closure.
+75        """
+76        if self.conn and not self.conn.closed:
+77            self.conn.close()
+78            pass
+
+ + +

A helper method to close the connection, also useful for manual closure.

+
+ + +
+ +
+
+ + \ No newline at end of file diff --git a/docs/reference/iLibrary/src/getInfoForLibrary.md b/docs/reference/iLibrary/src/getInfoForLibrary.md new file mode 100644 index 0000000..209812a --- /dev/null +++ b/docs/reference/iLibrary/src/getInfoForLibrary.md @@ -0,0 +1,604 @@ + + + + + + + iLibrary.src.getInfoForLibrary API documentation + + + + + + + + + +
+
+

+iLibrary.src.getInfoForLibrary

+ + + + + + +
 1import json
+ 2from datetime import datetime, date
+ 3from decimal import Decimal
+ 4
+ 5
+ 6class getInfoForLibrary:
+ 7    def __init__(self, connection):
+ 8        self.conn = connection
+ 9
+10    def _convert_to_json_ready(self, row, description):
+11        """Interne Hilfsmethode zur Typ-Konvertierung und Bereinigung."""
+12        row_dict = {}
+13        titles = [col[0] for col in description]
+14
+15        for i, value in enumerate(row):
+16            key = titles[i]
+17            # Typ-Prüfung für JSON-Serialisierung
+18            if isinstance(value, (datetime, date)):
+19                row_dict[key] = value.isoformat()
+20            elif isinstance(value, Decimal):
+21                row_dict[key] = float(value)
+22            elif isinstance(value, bytes):
+23                row_dict[key] = value.decode('utf-8', errors='replace')
+24            elif value is None:
+25                row_dict[key] = None
+26            else:
+27                # Entfernt unnötige Leerzeichen von CHAR-Feldern
+28                row_dict[key] = str(value).strip()
+29        return row_dict
+30
+31    def getLibraryInfo(self, library: str, wantJson=True):
+32        if not library or len(library) > 10:
+33            raise ValueError("Ungültiger Bibliotheksname (max. 10 Zeichen).")
+34
+35        sql_query = f"SELECT * FROM TABLE(QSYS2.LIBRARY_INFO(upper('{library}')))"
+36        try:
+37            with self.conn.cursor() as cursor:
+38                cursor.execute(sql_query)
+39                row = cursor.fetchone()
+40
+41                if not row:
+42                    error_msg = {"error": f"No data found for library: {library}"}
+43                    return json.dumps(error_msg, indent=4) if wantJson else ("error", error_msg["error"])
+44
+45                if wantJson:
+46                    return json.dumps(self._convert_to_json_ready(row, cursor.description), indent=4)
+47
+48                return row
+49        except Exception as e:
+50            print(f"Fehler bei getLibraryInfo: {e}")
+51            return None
+52
+53    def getFileInfo(self, library: str, qFiles: bool = False) -> str:
+54        if not library:
+55            return json.dumps([{"error": "A library name is required."}])
+56
+57        if qFiles:
+58            sql = f"SELECT * FROM QSYS2.SYSMEMBERSTAT WHERE SYSTEM_TABLE_SCHEMA = '{library.upper()}' AND SOURCE_TYPE IS NOT NULL ORDER BY SYSTEM_TABLE_MEMBER"
+59        else:
+60            sql = f"SELECT * FROM TABLE(QSYS2.OBJECT_STATISTICS('{library.upper()}', '*ALL')) AS X"
+61
+62        try:
+63            with self.conn.cursor() as cursor:
+64                cursor.execute(sql)
+65                rows = cursor.fetchall()
+66
+67                if not rows:
+68                    return json.dumps([{"error": f"No Files Found in Library: {library}"}])
+69
+70                result_list = [self._convert_to_json_ready(row, cursor.description) for row in rows]
+71                self.conn.commit()
+72                return json.dumps(result_list, indent=4)
+73        except Exception as e:
+74            if self.conn: self.conn.rollback()
+75            return json.dumps([{"error": f"Database Error: {str(e)}"}])
+76
+77    def getAllLibraries(self):
+78        # Hier nutzen wir nun auch die dynamische Spaltenerkennung statt der harten Liste
+79        sql = "SELECT * FROM TABLE(QSYS2.OBJECT_STATISTICS('*ALL', '*LIB')) AS X"
+80        try:
+81            with self.conn.cursor() as cursor:
+82                cursor.execute(sql)
+83                rows = cursor.fetchall()
+84
+85                if not rows:
+86                    return json.dumps([{"error": "No Libraries found"}])
+87
+88                result_list = [self._convert_to_json_ready(row, cursor.description) for row in rows]
+89                self.conn.commit()
+90                return json.dumps(result_list, indent=4)
+91        except Exception as e:
+92            print(f"Fehler bei getAllLibraries: {e}")
+93            if self.conn: self.conn.rollback()
+94            return False
+
+ + +
+
+ +
+ + class + getInfoForLibrary: + + + +
+ +
 7class getInfoForLibrary:
+ 8    def __init__(self, connection):
+ 9        self.conn = connection
+10
+11    def _convert_to_json_ready(self, row, description):
+12        """Interne Hilfsmethode zur Typ-Konvertierung und Bereinigung."""
+13        row_dict = {}
+14        titles = [col[0] for col in description]
+15
+16        for i, value in enumerate(row):
+17            key = titles[i]
+18            # Typ-Prüfung für JSON-Serialisierung
+19            if isinstance(value, (datetime, date)):
+20                row_dict[key] = value.isoformat()
+21            elif isinstance(value, Decimal):
+22                row_dict[key] = float(value)
+23            elif isinstance(value, bytes):
+24                row_dict[key] = value.decode('utf-8', errors='replace')
+25            elif value is None:
+26                row_dict[key] = None
+27            else:
+28                # Entfernt unnötige Leerzeichen von CHAR-Feldern
+29                row_dict[key] = str(value).strip()
+30        return row_dict
+31
+32    def getLibraryInfo(self, library: str, wantJson=True):
+33        if not library or len(library) > 10:
+34            raise ValueError("Ungültiger Bibliotheksname (max. 10 Zeichen).")
+35
+36        sql_query = f"SELECT * FROM TABLE(QSYS2.LIBRARY_INFO(upper('{library}')))"
+37        try:
+38            with self.conn.cursor() as cursor:
+39                cursor.execute(sql_query)
+40                row = cursor.fetchone()
+41
+42                if not row:
+43                    error_msg = {"error": f"No data found for library: {library}"}
+44                    return json.dumps(error_msg, indent=4) if wantJson else ("error", error_msg["error"])
+45
+46                if wantJson:
+47                    return json.dumps(self._convert_to_json_ready(row, cursor.description), indent=4)
+48
+49                return row
+50        except Exception as e:
+51            print(f"Fehler bei getLibraryInfo: {e}")
+52            return None
+53
+54    def getFileInfo(self, library: str, qFiles: bool = False) -> str:
+55        if not library:
+56            return json.dumps([{"error": "A library name is required."}])
+57
+58        if qFiles:
+59            sql = f"SELECT * FROM QSYS2.SYSMEMBERSTAT WHERE SYSTEM_TABLE_SCHEMA = '{library.upper()}' AND SOURCE_TYPE IS NOT NULL ORDER BY SYSTEM_TABLE_MEMBER"
+60        else:
+61            sql = f"SELECT * FROM TABLE(QSYS2.OBJECT_STATISTICS('{library.upper()}', '*ALL')) AS X"
+62
+63        try:
+64            with self.conn.cursor() as cursor:
+65                cursor.execute(sql)
+66                rows = cursor.fetchall()
+67
+68                if not rows:
+69                    return json.dumps([{"error": f"No Files Found in Library: {library}"}])
+70
+71                result_list = [self._convert_to_json_ready(row, cursor.description) for row in rows]
+72                self.conn.commit()
+73                return json.dumps(result_list, indent=4)
+74        except Exception as e:
+75            if self.conn: self.conn.rollback()
+76            return json.dumps([{"error": f"Database Error: {str(e)}"}])
+77
+78    def getAllLibraries(self):
+79        # Hier nutzen wir nun auch die dynamische Spaltenerkennung statt der harten Liste
+80        sql = "SELECT * FROM TABLE(QSYS2.OBJECT_STATISTICS('*ALL', '*LIB')) AS X"
+81        try:
+82            with self.conn.cursor() as cursor:
+83                cursor.execute(sql)
+84                rows = cursor.fetchall()
+85
+86                if not rows:
+87                    return json.dumps([{"error": "No Libraries found"}])
+88
+89                result_list = [self._convert_to_json_ready(row, cursor.description) for row in rows]
+90                self.conn.commit()
+91                return json.dumps(result_list, indent=4)
+92        except Exception as e:
+93            print(f"Fehler bei getAllLibraries: {e}")
+94            if self.conn: self.conn.rollback()
+95            return False
+
+ + + + +
+ +
+ + getInfoForLibrary(connection) + + + +
+ +
8    def __init__(self, connection):
+9        self.conn = connection
+
+ + + + +
+
+
+ conn + + +
+ + + + +
+
+ +
+ + def + getLibraryInfo(self, library: str, wantJson=True): + + + +
+ +
32    def getLibraryInfo(self, library: str, wantJson=True):
+33        if not library or len(library) > 10:
+34            raise ValueError("Ungültiger Bibliotheksname (max. 10 Zeichen).")
+35
+36        sql_query = f"SELECT * FROM TABLE(QSYS2.LIBRARY_INFO(upper('{library}')))"
+37        try:
+38            with self.conn.cursor() as cursor:
+39                cursor.execute(sql_query)
+40                row = cursor.fetchone()
+41
+42                if not row:
+43                    error_msg = {"error": f"No data found for library: {library}"}
+44                    return json.dumps(error_msg, indent=4) if wantJson else ("error", error_msg["error"])
+45
+46                if wantJson:
+47                    return json.dumps(self._convert_to_json_ready(row, cursor.description), indent=4)
+48
+49                return row
+50        except Exception as e:
+51            print(f"Fehler bei getLibraryInfo: {e}")
+52            return None
+
+ + + + +
+
+ +
+ + def + getFileInfo(self, library: str, qFiles: bool = False) -> str: + + + +
+ +
54    def getFileInfo(self, library: str, qFiles: bool = False) -> str:
+55        if not library:
+56            return json.dumps([{"error": "A library name is required."}])
+57
+58        if qFiles:
+59            sql = f"SELECT * FROM QSYS2.SYSMEMBERSTAT WHERE SYSTEM_TABLE_SCHEMA = '{library.upper()}' AND SOURCE_TYPE IS NOT NULL ORDER BY SYSTEM_TABLE_MEMBER"
+60        else:
+61            sql = f"SELECT * FROM TABLE(QSYS2.OBJECT_STATISTICS('{library.upper()}', '*ALL')) AS X"
+62
+63        try:
+64            with self.conn.cursor() as cursor:
+65                cursor.execute(sql)
+66                rows = cursor.fetchall()
+67
+68                if not rows:
+69                    return json.dumps([{"error": f"No Files Found in Library: {library}"}])
+70
+71                result_list = [self._convert_to_json_ready(row, cursor.description) for row in rows]
+72                self.conn.commit()
+73                return json.dumps(result_list, indent=4)
+74        except Exception as e:
+75            if self.conn: self.conn.rollback()
+76            return json.dumps([{"error": f"Database Error: {str(e)}"}])
+
+ + + + +
+
+ +
+ + def + getAllLibraries(self): + + + +
+ +
78    def getAllLibraries(self):
+79        # Hier nutzen wir nun auch die dynamische Spaltenerkennung statt der harten Liste
+80        sql = "SELECT * FROM TABLE(QSYS2.OBJECT_STATISTICS('*ALL', '*LIB')) AS X"
+81        try:
+82            with self.conn.cursor() as cursor:
+83                cursor.execute(sql)
+84                rows = cursor.fetchall()
+85
+86                if not rows:
+87                    return json.dumps([{"error": "No Libraries found"}])
+88
+89                result_list = [self._convert_to_json_ready(row, cursor.description) for row in rows]
+90                self.conn.commit()
+91                return json.dumps(result_list, indent=4)
+92        except Exception as e:
+93            print(f"Fehler bei getAllLibraries: {e}")
+94            if self.conn: self.conn.rollback()
+95            return False
+
+ + + + +
+
+
+ + \ No newline at end of file diff --git a/docs/reference/iLibrary/src/getUserInfoForUser.md b/docs/reference/iLibrary/src/getUserInfoForUser.md new file mode 100644 index 0000000..cdce757 --- /dev/null +++ b/docs/reference/iLibrary/src/getUserInfoForUser.md @@ -0,0 +1,659 @@ + + + + + + + iLibrary.src.getUserInfoForUser API documentation + + + + + + + + + +
+
+

+iLibrary.src.getUserInfoForUser

+ + + + + + +
  1from os.path import join
+  2import paramiko
+  3import pyodbc
+  4import json
+  5from datetime import datetime, date
+  6from decimal import Decimal
+  7
+  8class getUserInfoForUser():
+  9    """
+ 10    Handles user information retrieval and messaging functionalities.
+ 11
+ 12    This class provides methods to interact with the database for retrieving user information
+ 13    and to send messages to specified users. It supports data retrieval in different formats
+ 14    (e.g., JSON or tuple), and it enables system messaging with configurable options.
+ 15
+ 16    :ivar conn: Database connection object used for executing queries.
+ 17    :type conn: Any
+ 18    """
+ 19    def getAllUsers(self, wantJson: bool = False):
+ 20        """
+ 21        Retrieves all user information from the database. Optionally returns the data in
+ 22        JSON format depending on the provided parameter.
+ 23
+ 24        Retrieves a list of users stored in the database and can output the data either as
+ 25        a list of tuples or in JSON format. The query fetches all fields available in the
+ 26        user information database table and handles cases where no data is found.
+ 27
+ 28        :param wantJson: Boolean flag to indicate whether the result should be returned
+ 29            in JSON format. If set to False, the result will be a list of tuples. Default
+ 30            is False.
+ 31        :return: The data fetched from the database. When `wantJson` is True, returns a
+ 32            JSON object as a string. Otherwise, returns a list of tuples.
+ 33        """
+ 34        sql_query = "SELECT * FROM qsys2.user_info"
+ 35
+ 36        def json_serial(obj):
+ 37            if hasattr(obj, 'isoformat'):
+ 38                return obj.isoformat()
+ 39            return str(obj)
+ 40
+ 41        try:
+ 42            with self.conn.cursor() as cursor:
+ 43                cursor.execute(sql_query)
+ 44                rows = cursor.fetchall()
+ 45
+ 46                if not rows:
+ 47                    error_msg = {'error': 'No data found'}
+ 48                    return json.dumps(error_msg, indent=4) if wantJson else [("error", "No data found")]
+ 49
+ 50                # Get column names
+ 51                columns = [column[0] for column in cursor.description]
+ 52
+ 53                if wantJson:
+ 54                    # Create a LIST of dictionaries
+ 55                    results = [dict(zip(columns, r)) for r in rows]
+ 56                    return json.dumps(results, indent=4, default=json_serial)
+ 57
+ 58                return rows  # Returns the list of tuples
+ 59
+ 60        except Exception as e:
+ 61            print(f"An error occurred: {e}")
+ 62            return None
+ 63
+ 64    def getSingleUserInformation(self, username: str, wantJson: bool = False):
+ 65        """
+ 66        Retrieves information about a specific user from the database based on their username. The function supports
+ 67        returning data either as a JSON-formatted string or as a tuple with corresponding database fields.
+ 68
+ 69        :param username: The username of the database user whose information is to be retrieved. Must not be empty.
+ 70        :type username: str
+ 71        :param wantJson: Indicates whether the output should be formatted as JSON. Defaults to False.
+ 72        :type wantJson: bool
+ 73        :return: A tuple containing database fields if `wantJson` is False, or a JSON-formatted string if `wantJson` is True.
+ 74                 If no user is found, returns either a JSON-formatted error message or a tuple with error details, based on the
+ 75                 value of `wantJson`. Returns None if an exception occurs.
+ 76        :rtype: Union[tuple, str, None]
+ 77        :raises ValueError: If the `username` input is empty or None.
+ 78        """
+ 79        if not username:
+ 80          raise ValueError("A username is required.")
+ 81
+ 82        sql_query = f"SELECT * FROM qsys2.user_info WHERE AUTHORIZATION_NAME = upper('{username}')"
+ 83
+ 84        def json_serial(obj):
+ 85            # Handle datetime and Decimal (common in DB2)
+ 86            if hasattr(obj, 'isoformat'):
+ 87                return obj.isoformat()
+ 88            return str(obj)
+ 89
+ 90        try:
+ 91            with self.conn.cursor() as cursor:
+ 92                cursor.execute(sql_query)
+ 93                row = cursor.fetchone()  # Since you only expect one user
+ 94
+ 95                if not row:
+ 96                    error_msg = {'error': 'No data found for User: ' + username}
+ 97                    return json.dumps(error_msg, indent=4) if wantJson else ("error", error_msg['error'])
+ 98
+ 99                # DYNAMICALLY get column names from the database itself
+100                columns = [column[0] for column in cursor.description]
+101                row_dict = dict(zip(columns, row))
+102
+103                if wantJson:
+104                    return json.dumps(row_dict, indent=4, default=json_serial)
+105                return row  # Returns the tuple
+106
+107        except Exception as e:
+108            print(f"An error occurred: {e}")
+109            return None
+
+ + +
+
+ +
+ + class + getUserInfoForUser: + + + +
+ +
  9class getUserInfoForUser():
+ 10    """
+ 11    Handles user information retrieval and messaging functionalities.
+ 12
+ 13    This class provides methods to interact with the database for retrieving user information
+ 14    and to send messages to specified users. It supports data retrieval in different formats
+ 15    (e.g., JSON or tuple), and it enables system messaging with configurable options.
+ 16
+ 17    :ivar conn: Database connection object used for executing queries.
+ 18    :type conn: Any
+ 19    """
+ 20    def getAllUsers(self, wantJson: bool = False):
+ 21        """
+ 22        Retrieves all user information from the database. Optionally returns the data in
+ 23        JSON format depending on the provided parameter.
+ 24
+ 25        Retrieves a list of users stored in the database and can output the data either as
+ 26        a list of tuples or in JSON format. The query fetches all fields available in the
+ 27        user information database table and handles cases where no data is found.
+ 28
+ 29        :param wantJson: Boolean flag to indicate whether the result should be returned
+ 30            in JSON format. If set to False, the result will be a list of tuples. Default
+ 31            is False.
+ 32        :return: The data fetched from the database. When `wantJson` is True, returns a
+ 33            JSON object as a string. Otherwise, returns a list of tuples.
+ 34        """
+ 35        sql_query = "SELECT * FROM qsys2.user_info"
+ 36
+ 37        def json_serial(obj):
+ 38            if hasattr(obj, 'isoformat'):
+ 39                return obj.isoformat()
+ 40            return str(obj)
+ 41
+ 42        try:
+ 43            with self.conn.cursor() as cursor:
+ 44                cursor.execute(sql_query)
+ 45                rows = cursor.fetchall()
+ 46
+ 47                if not rows:
+ 48                    error_msg = {'error': 'No data found'}
+ 49                    return json.dumps(error_msg, indent=4) if wantJson else [("error", "No data found")]
+ 50
+ 51                # Get column names
+ 52                columns = [column[0] for column in cursor.description]
+ 53
+ 54                if wantJson:
+ 55                    # Create a LIST of dictionaries
+ 56                    results = [dict(zip(columns, r)) for r in rows]
+ 57                    return json.dumps(results, indent=4, default=json_serial)
+ 58
+ 59                return rows  # Returns the list of tuples
+ 60
+ 61        except Exception as e:
+ 62            print(f"An error occurred: {e}")
+ 63            return None
+ 64
+ 65    def getSingleUserInformation(self, username: str, wantJson: bool = False):
+ 66        """
+ 67        Retrieves information about a specific user from the database based on their username. The function supports
+ 68        returning data either as a JSON-formatted string or as a tuple with corresponding database fields.
+ 69
+ 70        :param username: The username of the database user whose information is to be retrieved. Must not be empty.
+ 71        :type username: str
+ 72        :param wantJson: Indicates whether the output should be formatted as JSON. Defaults to False.
+ 73        :type wantJson: bool
+ 74        :return: A tuple containing database fields if `wantJson` is False, or a JSON-formatted string if `wantJson` is True.
+ 75                 If no user is found, returns either a JSON-formatted error message or a tuple with error details, based on the
+ 76                 value of `wantJson`. Returns None if an exception occurs.
+ 77        :rtype: Union[tuple, str, None]
+ 78        :raises ValueError: If the `username` input is empty or None.
+ 79        """
+ 80        if not username:
+ 81          raise ValueError("A username is required.")
+ 82
+ 83        sql_query = f"SELECT * FROM qsys2.user_info WHERE AUTHORIZATION_NAME = upper('{username}')"
+ 84
+ 85        def json_serial(obj):
+ 86            # Handle datetime and Decimal (common in DB2)
+ 87            if hasattr(obj, 'isoformat'):
+ 88                return obj.isoformat()
+ 89            return str(obj)
+ 90
+ 91        try:
+ 92            with self.conn.cursor() as cursor:
+ 93                cursor.execute(sql_query)
+ 94                row = cursor.fetchone()  # Since you only expect one user
+ 95
+ 96                if not row:
+ 97                    error_msg = {'error': 'No data found for User: ' + username}
+ 98                    return json.dumps(error_msg, indent=4) if wantJson else ("error", error_msg['error'])
+ 99
+100                # DYNAMICALLY get column names from the database itself
+101                columns = [column[0] for column in cursor.description]
+102                row_dict = dict(zip(columns, row))
+103
+104                if wantJson:
+105                    return json.dumps(row_dict, indent=4, default=json_serial)
+106                return row  # Returns the tuple
+107
+108        except Exception as e:
+109            print(f"An error occurred: {e}")
+110            return None
+
+ + +

Handles user information retrieval and messaging functionalities.

+ +

This class provides methods to interact with the database for retrieving user information +and to send messages to specified users. It supports data retrieval in different formats +(e.g., JSON or tuple), and it enables system messaging with configurable options.

+ +

:ivar conn: Database connection object used for executing queries.

+
+ + +
+ +
+ + def + getAllUsers(self, wantJson: bool = False): + + + +
+ +
20    def getAllUsers(self, wantJson: bool = False):
+21        """
+22        Retrieves all user information from the database. Optionally returns the data in
+23        JSON format depending on the provided parameter.
+24
+25        Retrieves a list of users stored in the database and can output the data either as
+26        a list of tuples or in JSON format. The query fetches all fields available in the
+27        user information database table and handles cases where no data is found.
+28
+29        :param wantJson: Boolean flag to indicate whether the result should be returned
+30            in JSON format. If set to False, the result will be a list of tuples. Default
+31            is False.
+32        :return: The data fetched from the database. When `wantJson` is True, returns a
+33            JSON object as a string. Otherwise, returns a list of tuples.
+34        """
+35        sql_query = "SELECT * FROM qsys2.user_info"
+36
+37        def json_serial(obj):
+38            if hasattr(obj, 'isoformat'):
+39                return obj.isoformat()
+40            return str(obj)
+41
+42        try:
+43            with self.conn.cursor() as cursor:
+44                cursor.execute(sql_query)
+45                rows = cursor.fetchall()
+46
+47                if not rows:
+48                    error_msg = {'error': 'No data found'}
+49                    return json.dumps(error_msg, indent=4) if wantJson else [("error", "No data found")]
+50
+51                # Get column names
+52                columns = [column[0] for column in cursor.description]
+53
+54                if wantJson:
+55                    # Create a LIST of dictionaries
+56                    results = [dict(zip(columns, r)) for r in rows]
+57                    return json.dumps(results, indent=4, default=json_serial)
+58
+59                return rows  # Returns the list of tuples
+60
+61        except Exception as e:
+62            print(f"An error occurred: {e}")
+63            return None
+
+ + +

Retrieves all user information from the database. Optionally returns the data in +JSON format depending on the provided parameter.

+ +

Retrieves a list of users stored in the database and can output the data either as +a list of tuples or in JSON format. The query fetches all fields available in the +user information database table and handles cases where no data is found.

+ +
Parameters
+ +
    +
  • wantJson: Boolean flag to indicate whether the result should be returned +in JSON format. If set to False, the result will be a list of tuples. Default +is False.
  • +
+ +
Returns
+ +
+

The data fetched from the database. When wantJson is True, returns a + JSON object as a string. Otherwise, returns a list of tuples.

+
+
+ + +
+
+ +
+ + def + getSingleUserInformation(self, username: str, wantJson: bool = False): + + + +
+ +
 65    def getSingleUserInformation(self, username: str, wantJson: bool = False):
+ 66        """
+ 67        Retrieves information about a specific user from the database based on their username. The function supports
+ 68        returning data either as a JSON-formatted string or as a tuple with corresponding database fields.
+ 69
+ 70        :param username: The username of the database user whose information is to be retrieved. Must not be empty.
+ 71        :type username: str
+ 72        :param wantJson: Indicates whether the output should be formatted as JSON. Defaults to False.
+ 73        :type wantJson: bool
+ 74        :return: A tuple containing database fields if `wantJson` is False, or a JSON-formatted string if `wantJson` is True.
+ 75                 If no user is found, returns either a JSON-formatted error message or a tuple with error details, based on the
+ 76                 value of `wantJson`. Returns None if an exception occurs.
+ 77        :rtype: Union[tuple, str, None]
+ 78        :raises ValueError: If the `username` input is empty or None.
+ 79        """
+ 80        if not username:
+ 81          raise ValueError("A username is required.")
+ 82
+ 83        sql_query = f"SELECT * FROM qsys2.user_info WHERE AUTHORIZATION_NAME = upper('{username}')"
+ 84
+ 85        def json_serial(obj):
+ 86            # Handle datetime and Decimal (common in DB2)
+ 87            if hasattr(obj, 'isoformat'):
+ 88                return obj.isoformat()
+ 89            return str(obj)
+ 90
+ 91        try:
+ 92            with self.conn.cursor() as cursor:
+ 93                cursor.execute(sql_query)
+ 94                row = cursor.fetchone()  # Since you only expect one user
+ 95
+ 96                if not row:
+ 97                    error_msg = {'error': 'No data found for User: ' + username}
+ 98                    return json.dumps(error_msg, indent=4) if wantJson else ("error", error_msg['error'])
+ 99
+100                # DYNAMICALLY get column names from the database itself
+101                columns = [column[0] for column in cursor.description]
+102                row_dict = dict(zip(columns, row))
+103
+104                if wantJson:
+105                    return json.dumps(row_dict, indent=4, default=json_serial)
+106                return row  # Returns the tuple
+107
+108        except Exception as e:
+109            print(f"An error occurred: {e}")
+110            return None
+
+ + +

Retrieves information about a specific user from the database based on their username. The function supports +returning data either as a JSON-formatted string or as a tuple with corresponding database fields.

+ +
Parameters
+ +
    +
  • username: The username of the database user whose information is to be retrieved. Must not be empty.
  • +
  • wantJson: Indicates whether the output should be formatted as JSON. Defaults to False.
  • +
+ +
Returns
+ +
+

A tuple containing database fields if wantJson is False, or a JSON-formatted string if wantJson is True. + If no user is found, returns either a JSON-formatted error message or a tuple with error details, based on the + value of wantJson. Returns None if an exception occurs.

+
+ +
Raises
+ +
    +
  • ValueError: If the username input is empty or None.
  • +
+
+ + +
+
+
+ + \ No newline at end of file diff --git a/docs/reference/iLibrary/src/saveLibrary.md b/docs/reference/iLibrary/src/saveLibrary.md new file mode 100644 index 0000000..5cb37ab --- /dev/null +++ b/docs/reference/iLibrary/src/saveLibrary.md @@ -0,0 +1,1466 @@ + + + + + + + iLibrary.src.saveLibrary API documentation + + + + + + + + + +
+
+

+iLibrary.src.saveLibrary

+ + + + + + +
  1from _ast import Raise
+  2from os.path import join
+  3import paramiko
+  4import pyodbc
+  5import json
+  6from datetime import datetime, date
+  7from decimal import Decimal
+  8from typing import Union
+  9from pathlib import PureWindowsPath
+ 10
+ 11
+ 12class saveLibrary:
+ 13
+ 14    def saveLibrary(self,
+ 15                    library: str,
+ 16                    saveFileName: str,
+ 17                    dev: str = None,
+ 18                    vol: str = None,
+ 19                    toLibrary: str = None,
+ 20                    description: str = None,
+ 21                    localPath: str = None,
+ 22                    remPath: str = None,
+ 23                    getZip: bool = False,
+ 24                    port: int = None,
+ 25                    remSavf=True,
+ 26                    version: str = None,
+ 27                    max_records: Union[int, str, None] = None,
+ 28                    asp: Union[int, str, None] = None,
+ 29                    waitFile: Union[int, str, None] = None,
+ 30                    share: str = None,
+ 31                    authority: str = None
+ 32                    ) -> bool:
+ 33        """
+ 34        Saves a library to a specified save file, providing options for further customization such
+ 35        as setting the target release, saving as a zip file, specifying the device, volume, and more.
+ 36
+ 37        :param library: The name of the library to be saved. Must be a valid library name or one of
+ 38            the predefined options such as '*NONSYS', '*ALLUSR', '*IBM', etc.
+ 39        :type library: str
+ 40        :param saveFileName: The name of the save file where the library will be saved.
+ 41        :type saveFileName: str
+ 42        :param dev: The target device for the save operation. Defaults to '*SAVF' if not provided.
+ 43        :type dev: str, optional
+ 44        :param vol: Specifies the volume to be used. Use ‘*MOUNTED’ to refer to the mounted volume.
+ 45        :type vol: str, optional
+ 46        :param toLibrary: Target library where the save file will be temporarily stored. Defaults
+ 47            to the value of `library` if not specified.
+ 48        :type toLibrary: str, optional
+ 49        :param description: An optional description for the save file to be created.
+ 50        :type description: str, optional
+ 51        :param localPath: The local path where the save file will be downloaded if `getZip` is set
+ 52            to True. Must be an absolute path.
+ 53        :type localPath: str, optional
+ 54        :param remPath: The remote directory path on the target system to temporarily store the
+ 55            save file if `getZip` is set to True. Must be an absolute path.
+ 56        :type remPath: str, optional
+ 57        :param getZip: A flag that determines whether the save file should be archived into a zip
+ 58            file and downloaded locally.
+ 59        :type getZip: bool
+ 60        :param port: Specifies the port to be used for transferring the save file when `getZip` is
+ 61            enabled.
+ 62        :type port: int, optional
+ 63        :param remSavf: A flag indicating whether the save file should be removed from the remote
+ 64            target system after a successful save.
+ 65        :type remSavf: bool
+ 66        :param version: The target release version for the save operation. Valid values include
+ 67            ‘*CURRENT’, or specific OS versions like 'V1R1M0', 'V2R3M0', and so on.
+ 68        :type version: str, optional
+ 69        :param max_records: Optional parameter for specifying the maximum number of records in
+ 70            the save file.
+ 71        :type max_records: Union[int, str, None], optional
+ 72        :param asp: Auxiliary storage pool (ASP) device number or name if applicable.
+ 73        :type asp: Union[int, str, None], optional
+ 74        :param waitFile: The amount of time to wait for file access locks to be released.
+ 75        :type waitFile: Union[int, str, None], optional
+ 76        :param share: Specifies the share handling for threads or users accessing the save file.
+ 77        :type share: str, optional
+ 78        :param authority: Authority option to set for the save file being saved.
+ 79        :type authority: str, optional
+ 80        :return: A boolean indicating whether the library was successfully saved. Returns True on
+ 81            success or False on failure.
+ 82        :rtype: bool
+ 83        """
+ 84        # Target Release List
+ 85        trgList: list = ["V1R1M0", "V1R1M2", "V1R2M0", "V1R3M0", "V2R1M0", "V2R1M1",
+ 86                         "V2R2M0", "V2R3M0", "V3R0M5", "V3R1M0", "V3R2M0", "V3R6M0",
+ 87                         "V3R7M0", "V4R1M0", "V4R2M0", "V4R3M0", "V4R4M0", "V4R5M0",
+ 88                         "V5R1M0", "V5R2M0", "V5R3M0", "V5R4M0", "V6R1M0", "V6R1M1",
+ 89                         "V7R1M0", "V7R2M0", "V7R3M0", "V7R4M0", "V7R5M0", "V7R6M0"]
+ 90
+ 91        # check if something missing from the Arguments
+ 92        # check if Library is empty or not
+ 93        if not library:
+ 94            raise ValueError("A library name is required.")
+ 95        # check if saveFileName is empty or not
+ 96        if not saveFileName:
+ 97            raise ValueError("A save file name is required.")
+ 98        # check if toLibrary is empty or not
+ 99        if not toLibrary:
+100            toLibrary = library
+101        # check if user want the SaveFile as ZIP File
+102        if getZip:
+103            if not remPath:
+104                raise ValueError("A remote path is required. Use 'remPath' instead.")
+105            elif remPath[-1] == '/':
+106                remPath = remPath[:-1]
+107            if not localPath:
+108                raise ValueError("A local path is required. Use 'localPath' instead.")
+109            elif localPath[-1] == '/':
+110                localPath = localPath[:-1]
+111        # check wich Version of SaveFile is wanted
+112        if not version in list(trgList):
+113            version = "*CURRENT"
+114        else:
+115            version = version.upper()
+116        command_str: str = f'SAVLIB'
+117
+118        # check if Library is valid or not
+119        validated_library = self.__validate_max_value(value=library, param_name='library',
+120                                                      str_format=['*NONSYS', '*ALLUSR', '*IBM', '*SELECT', '*USRSPC',
+121                                                                  library])
+122        if validated_library:
+123            command_str += f' LIB({validated_library})'
+124        else:
+125            library_str = str(library)
+126            raise ValueError(
+127                f"The library '{library_str}' is not valid. Must be one of the specified strings or a valid number.")
+128        # check Dev - Device
+129        if not dev in ['*SAVF', '*MEDDFN']:
+130            command_str += f' DEV(*SAVF)'
+131        else:
+132            command_str += f' DEV({dev.upper()})'
+133        if vol is not None and vol == '*MOUNTED':
+134            command_str += f' VOL({vol})'
+135        # starting with mem main Sourcecode of saveLLibrary
+136        if self.__crtsavf(saveFileName, toLibrary, description, max_records=max_records, asp=asp, waitFile=waitFile,
+137                          share=share, authority=authority):
+138            # command_str: str = f"SAVLIB LIB({library.strip()}) DEV(*SAVF) SAVF({toLibrary.strip()}/{saveFileName.strip()}) TGTRLS({version.strip()})"
+139            command_str += f" SAVF({toLibrary.strip()}/{saveFileName.strip()}) TGTRLS({version.strip()})"
+140            #print(command_str)
+141            try:
+142                with self.conn.cursor() as cursor:
+143                    # execute the Command for creating a Savefile.
+144                    cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str))
+145                    if getZip:
+146                        try:
+147                            remote_temp_savf_path = join(remPath, saveFileName.upper() + '.savf')
+148
+149                            destination_local_path = join(localPath, saveFileName.upper() + '.savf')
+150                            command_str = (
+151                                f"CPYTOSTMF FROMMBR('/QSYS.LIB/{toLibrary.upper().strip()}.LIB/{saveFileName.upper().strip()}.FILE') "
+152                                f"TOSTMF('{remote_temp_savf_path.strip()}') STMFOPT(*REPLACE)"
+153                            )
+154
+155                            # Execute the command on the remote system
+156                            cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str,))
+157
+158                            if self.__getSavFile(localFilePath=destination_local_path,
+159                                                 remotePath=remote_temp_savf_path, port=port):
+160                                rmvCommand = f"QSH CMD('rm -r {remote_temp_savf_path}')"
+161                                cursor.execute("CALL QSYS2.QCMDEXC(?)", (rmvCommand))
+162                            else:
+163                                raise ValueError("Something went wrong. With downloading the Save File.")
+164                            if remSavf:
+165                                if not self.removeFile(library=toLibrary, saveFileName=saveFileName):
+166                                    raise ValueError(f"The Save File {saveFileName} was not successfully removed.")
+167
+168                        except Exception as e:
+169                            self.__handle_error(error=e, pgm="saveLibrary - Transfer")
+170
+171            except Exception as e:
+172                self.__handle_error(error=e, pgm="saveLibrary")
+173                self.conn.rollback()
+174                return False
+175            else:
+176                self.conn.commit()
+177                if getZip:
+178                    print(f"File successfully downloaded to: {destination_local_path}")
+179                    return True
+180
+181                print(f"Successfully saved in the Library '{library}' successfully.")
+182                return True
+183
+184        return False
+185
+186    # ------------------------------------------------------
+187    # sub Function: create the Savefile on the AS400
+188    # ------------------------------------------------------
+189    def __crtsavf(self,
+190                  saveFileName: str,
+191                  library: str,
+192                  description: str = None,
+193                  max_records: Union[int, str, None] = None,
+194                  asp: Union[int, str, None] = None,
+195                  waitFile: Union[int, str, None] = None,
+196                  share: str = None,
+197                  authority: str = None
+198                  ) -> bool:
+199        """
+200            Sub-function to create a save file on the IBM i server.
+201
+202            This function executes the `CRTSAVF` (Create Save File) CL command
+203            to create a new save file in the specified library. This is a
+204            prerequisite for saving a library's contents.
+205
+206            Args:
+207                saveFileName (str): The name of the save file to be created.
+208                                    This will be the AS/400 object name.
+209                library (str): The name of the library where the save file will be created.
+210                description (str, optional): A text description for the save file. Defaults to None.
+211
+212            Returns:
+213                bool: True if the save file was created successfully, False otherwise.
+214        """
+215        # check is a parameter empty or not
+216
+217        if not saveFileName:
+218            raise ValueError("A file name is required.")
+219        if not library:
+220            raise ValueError("A library name is required.")
+221        if not description:
+222            description = 'A SaveFile from iLibrary'
+223
+224        command_str: str = f"CRTSAVF FILE({library.upper().strip()}/{saveFileName.upper().strip()}) TEXT('{description.strip()}')"
+225
+226        # check max_records for MAXRCDS parameter
+227        if self.__validate_max_value(value=max_records, param_name='max_records', str_format=['*NOMAX'],
+228                                     max_limit=4293525600) and not None:
+229            command_str += f" MAXRCDS({max_records})"
+230        # check asp for ASP 2147483647
+231        if self.__validate_max_value(value=asp, param_name='asp', str_format=['*LIBASP'], max_limit=32) and not None:
+232            command_str += f" ASP({asp})"
+233        if self.__validate_max_value(value=waitFile, param_name='waitFile', str_format=['*IMMED', '*CLS'],
+234                                     max_limit=2147483647) and not None:
+235            command_str += f" WAITFILE({waitFile})"
+236        if self.__validate_max_value(value=share, param_name='share', str_format=['*YES', '*NO']) and not None:
+237            command_str += f" SHARE({share})"
+238
+239        if authority is not None:
+240            upper_authority = authority.upper()
+241
+242            # 1. Check for custom authority (not in list AND up to 10 chars)
+243            if upper_authority not in ['*EXCLUDE', '*ALL', '*CHANGE', '*LIBCRTAUT', '*USE'] and len(
+244                    upper_authority) <= 10:
+245                # **CORRECTION 1: Use upper_authority here, not the undefined 'auth'**
+246                command_str += f" AUT({upper_authority})"
+247                # The 'pass' statements are redundant and can be removed
+248
+249            # 2. Add an 'elif' to handle the case where it IS one of the standard values
+250            elif upper_authority in ['*EXCLUDE', '*ALL', '*CHANGE', '*LIBCRTAUT', '*USE']:
+251                command_str += f" AUT({upper_authority})"
+252
+253
+254        try:
+255            with self.conn.cursor() as cursor:
+256                # execute the Command for creating a Savefile.
+257                cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str))
+258
+259        except Exception as e:
+260            self.__handle_error(error=e, pgm="__crtsavf")
+261            # remove a SAVF if its exists and we got an error
+262            if e.args[0] == 'HY000':
+263                sql = """
+264                      SELECT 1
+265                      FROM QSYS2.SAVE_FILE_INFO
+266                      WHERE SAVE_FILE_LIBRARY = ? \
+267                        AND SAVE_FILE = ?
+268                          FETCH FIRST 1 ROW ONLY \
+269                      """
+270                cursor = self.conn.cursor()
+271                cursor.execute(sql, library, saveFileName)
+272                result = cursor.fetchone()
+273                if result is not None:
+274                    self.removeFile(library=library, saveFileName=saveFileName)
+275            self.conn.rollback()
+276            raise ValueError(e)
+277        else:
+278            self.conn.commit()
+279            return True
+280
+281    # --------------------------------------------------------------------------
+282    # __validate_max_value - Helper Function for checking parameter
+283    # --------------------------------------------------------------------------
+284    def __validate_max_value(self,
+285                             value: Union[int, str, None],
+286                             param_name: str,
+287                             str_format: list[str],
+288                             min_limit: int = 1,
+289                             max_limit: int = None
+290                             ) -> Union[int, str, bool]:  # Includes bool as requested
+291        """
+292        Validates an input value for 'MAX' type parameters against a custom range.
+293        Handles special strings defined in str_format and numeric values.
+294
+295        Returns: The validated integer, the standardized special string, or False on failure (if no exception is raised).
+296        Raises: ValueError for invalid string format or out-of-range number.
+297        """
+298
+299        # Helper for clear error messages
+300        str_options = ", ".join([f"'{s}'" for s in str_format])
+301
+302        # 1. Handle special string
+303        if isinstance(value, str):
+304            upper_value = value.upper()
+305
+306            for special_value in str_format:
+307                normalized_special_value = special_value.upper()
+308
+309                if upper_value == special_value.upper() or upper_value == normalized_special_value:
+310                    # Found a match! Return the official, fully formatted string.
+311                    return special_value
+312
+313        # 2. Attempt Numeric Conversion (handles int and string-of-int)
+314        if value is not None:
+315            try:
+316                numeric_value = int(value)
+317            except ValueError:
+318                # Value is an invalid string (e.g., 'hello')
+319                raise ValueError(
+320                    f"Invalid value for {param_name}. Must be '{str_format}' or a number "
+321                    f"between {min_limit} and {max_limit:,}."
+322                )
+323        else:
+324            # If the value is None
+325            return False
+326
+327        # 3. Check Numeric Range
+328        if min_limit <= numeric_value <= max_limit:
+329            return numeric_value
+330        else:
+331            # Number is out of range
+332            raise ValueError(
+333                f"Invalid numeric value for {param_name}. Must be between {min_limit} and {max_limit:,}. "
+334                f"Received: {numeric_value}"
+335            )
+336
+337    # ------------------------------------------------------
+338    # getZipFile - getting the Zipfile from the SaveFile
+339    # ------------------------------------------------------
+340    def __getSavFile(self,
+341                     localFilePath: str,
+342                     remotePath: str,
+343                     port: int = None
+344                     ) -> bool:
+345        """
+346            Downloads a file from the remote IBM i via SFTP.
+347
+348            This method uses Paramiko to establish a secure shell (SSH) connection and
+349            then an SFTP session to transfer a file from a specified remote location
+350            on the IBM i's IFS to a local path.
+351
+352            Args:
+353                localFilePath (str): The full path to the file on the remote IBM i's IFS.
+354                remotePath (str): The full path on the local machine where the file
+355                                       will be saved. For example, '/Users/user/Documents/somefile.savf'.
+356                port (int, optional): The port to connect to the IBMi server. Defaults to None.
+357
+358            Returns:
+359                bool: True if the file was downloaded successfully, False otherwise.
+360
+361            Raises:
+362                ValueError: If either the remote_file_path or local_save_path is not provided.
+363        """
+364        if not localFilePath:
+365            print("Error: A local file path is required.")
+366            return False
+367        if not remotePath:
+368            print("Error: A remote path is required.")
+369            return False
+370        if not port:
+371            port = 2222
+372
+373        remotePath = PureWindowsPath(remotePath).as_posix()
+374        ssh_client = paramiko.SSHClient()
+375
+376        ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+377
+378        try:
+379            with ssh_client:
+380                ssh_client.connect(
+381                    hostname=self.db_host,
+382                    username=self.db_user,
+383                    password=self.db_password,
+384                    port=port
+385                )
+386                with ssh_client.open_sftp() as ftp_client:
+387                    ftp_client.get(remotePath, localFilePath)
+388                    return True
+389
+390        except paramiko.ssh_exception.AuthenticationException as e:
+391            print(f"Authentication failed. Check your username and password: {e}")
+392            return False
+393        except paramiko.ssh_exception.SSHException as e:
+394            print(f"SSH error occurred: {e}")
+395            return False
+396        except FileNotFoundError as e:
+397            print(f"File not found on the remote host: {e}")
+398            return False
+399
+400        finally:
+401            pass
+402
+403    def removeFile(self, library: str, saveFileName: str) -> bool:
+404        """
+405        Removes a save file from the specified library.
+406
+407        This function executes the system command to delete a save file from an IBM i
+408        system. It connects to the database through a cursor, and attempts to perform
+409        the operation. If an error is encountered during execution, the function
+410        rolls back the transaction and logs the error. On success, the transaction
+411        is committed.
+412
+413        :param library: The name of the library containing the save file to be removed.
+414        :type library: str
+415        :param saveFileName: The name of the save file to be removed.
+416        :type saveFileName: str
+417        :return: True if the save file is removed successfully, otherwise False.
+418        :rtype: bool
+419        """
+420        command_str: str = f"DLTF FILE({library.upper()}/{saveFileName.upper()})"
+421        try:
+422            with self.conn.cursor() as cursor:
+423                # execute the Command for deleting a Savefile.
+424                cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str))
+425
+426        except Exception as e:
+427            self.__handle_error(error=e, pgm="removeFile")
+428            self.conn.rollback()
+429            return False
+430        else:
+431            self.conn.commit()
+432            return True
+433
+434    def __handle_error(self, error, pgm: str):
+435        """
+436        Handles errors encountered during the execution of a command.
+437
+438        This method processes an error raised during the execution of a command in a
+439        specific function and extracts detailed error information including SQLSTATE
+440        and the error message. The formatted details are printed to the console for
+441        debugging purposes.
+442
+443        :param error: The error object encountered during command execution.
+444        :type error: Exception
+445        :param pgm: The name of the function where the error occurred.
+446        :type pgm: str
+447        :return: None
+448        """
+449        print("-------------------------------------------------------------")
+450        print(f"An error occurred while executing command in function {pgm}:")
+451        sqlstate = error.args[0]
+452        error_message = error.args[1]
+453
+454        print(f"SQLSTATE: {sqlstate}")
+455        print(f"Message: {error_message}")
+
+ + +
+
+ +
+ + class + saveLibrary: + + + +
+ +
 13class saveLibrary:
+ 14
+ 15    def saveLibrary(self,
+ 16                    library: str,
+ 17                    saveFileName: str,
+ 18                    dev: str = None,
+ 19                    vol: str = None,
+ 20                    toLibrary: str = None,
+ 21                    description: str = None,
+ 22                    localPath: str = None,
+ 23                    remPath: str = None,
+ 24                    getZip: bool = False,
+ 25                    port: int = None,
+ 26                    remSavf=True,
+ 27                    version: str = None,
+ 28                    max_records: Union[int, str, None] = None,
+ 29                    asp: Union[int, str, None] = None,
+ 30                    waitFile: Union[int, str, None] = None,
+ 31                    share: str = None,
+ 32                    authority: str = None
+ 33                    ) -> bool:
+ 34        """
+ 35        Saves a library to a specified save file, providing options for further customization such
+ 36        as setting the target release, saving as a zip file, specifying the device, volume, and more.
+ 37
+ 38        :param library: The name of the library to be saved. Must be a valid library name or one of
+ 39            the predefined options such as '*NONSYS', '*ALLUSR', '*IBM', etc.
+ 40        :type library: str
+ 41        :param saveFileName: The name of the save file where the library will be saved.
+ 42        :type saveFileName: str
+ 43        :param dev: The target device for the save operation. Defaults to '*SAVF' if not provided.
+ 44        :type dev: str, optional
+ 45        :param vol: Specifies the volume to be used. Use ‘*MOUNTED’ to refer to the mounted volume.
+ 46        :type vol: str, optional
+ 47        :param toLibrary: Target library where the save file will be temporarily stored. Defaults
+ 48            to the value of `library` if not specified.
+ 49        :type toLibrary: str, optional
+ 50        :param description: An optional description for the save file to be created.
+ 51        :type description: str, optional
+ 52        :param localPath: The local path where the save file will be downloaded if `getZip` is set
+ 53            to True. Must be an absolute path.
+ 54        :type localPath: str, optional
+ 55        :param remPath: The remote directory path on the target system to temporarily store the
+ 56            save file if `getZip` is set to True. Must be an absolute path.
+ 57        :type remPath: str, optional
+ 58        :param getZip: A flag that determines whether the save file should be archived into a zip
+ 59            file and downloaded locally.
+ 60        :type getZip: bool
+ 61        :param port: Specifies the port to be used for transferring the save file when `getZip` is
+ 62            enabled.
+ 63        :type port: int, optional
+ 64        :param remSavf: A flag indicating whether the save file should be removed from the remote
+ 65            target system after a successful save.
+ 66        :type remSavf: bool
+ 67        :param version: The target release version for the save operation. Valid values include
+ 68            ‘*CURRENT’, or specific OS versions like 'V1R1M0', 'V2R3M0', and so on.
+ 69        :type version: str, optional
+ 70        :param max_records: Optional parameter for specifying the maximum number of records in
+ 71            the save file.
+ 72        :type max_records: Union[int, str, None], optional
+ 73        :param asp: Auxiliary storage pool (ASP) device number or name if applicable.
+ 74        :type asp: Union[int, str, None], optional
+ 75        :param waitFile: The amount of time to wait for file access locks to be released.
+ 76        :type waitFile: Union[int, str, None], optional
+ 77        :param share: Specifies the share handling for threads or users accessing the save file.
+ 78        :type share: str, optional
+ 79        :param authority: Authority option to set for the save file being saved.
+ 80        :type authority: str, optional
+ 81        :return: A boolean indicating whether the library was successfully saved. Returns True on
+ 82            success or False on failure.
+ 83        :rtype: bool
+ 84        """
+ 85        # Target Release List
+ 86        trgList: list = ["V1R1M0", "V1R1M2", "V1R2M0", "V1R3M0", "V2R1M0", "V2R1M1",
+ 87                         "V2R2M0", "V2R3M0", "V3R0M5", "V3R1M0", "V3R2M0", "V3R6M0",
+ 88                         "V3R7M0", "V4R1M0", "V4R2M0", "V4R3M0", "V4R4M0", "V4R5M0",
+ 89                         "V5R1M0", "V5R2M0", "V5R3M0", "V5R4M0", "V6R1M0", "V6R1M1",
+ 90                         "V7R1M0", "V7R2M0", "V7R3M0", "V7R4M0", "V7R5M0", "V7R6M0"]
+ 91
+ 92        # check if something missing from the Arguments
+ 93        # check if Library is empty or not
+ 94        if not library:
+ 95            raise ValueError("A library name is required.")
+ 96        # check if saveFileName is empty or not
+ 97        if not saveFileName:
+ 98            raise ValueError("A save file name is required.")
+ 99        # check if toLibrary is empty or not
+100        if not toLibrary:
+101            toLibrary = library
+102        # check if user want the SaveFile as ZIP File
+103        if getZip:
+104            if not remPath:
+105                raise ValueError("A remote path is required. Use 'remPath' instead.")
+106            elif remPath[-1] == '/':
+107                remPath = remPath[:-1]
+108            if not localPath:
+109                raise ValueError("A local path is required. Use 'localPath' instead.")
+110            elif localPath[-1] == '/':
+111                localPath = localPath[:-1]
+112        # check wich Version of SaveFile is wanted
+113        if not version in list(trgList):
+114            version = "*CURRENT"
+115        else:
+116            version = version.upper()
+117        command_str: str = f'SAVLIB'
+118
+119        # check if Library is valid or not
+120        validated_library = self.__validate_max_value(value=library, param_name='library',
+121                                                      str_format=['*NONSYS', '*ALLUSR', '*IBM', '*SELECT', '*USRSPC',
+122                                                                  library])
+123        if validated_library:
+124            command_str += f' LIB({validated_library})'
+125        else:
+126            library_str = str(library)
+127            raise ValueError(
+128                f"The library '{library_str}' is not valid. Must be one of the specified strings or a valid number.")
+129        # check Dev - Device
+130        if not dev in ['*SAVF', '*MEDDFN']:
+131            command_str += f' DEV(*SAVF)'
+132        else:
+133            command_str += f' DEV({dev.upper()})'
+134        if vol is not None and vol == '*MOUNTED':
+135            command_str += f' VOL({vol})'
+136        # starting with mem main Sourcecode of saveLLibrary
+137        if self.__crtsavf(saveFileName, toLibrary, description, max_records=max_records, asp=asp, waitFile=waitFile,
+138                          share=share, authority=authority):
+139            # command_str: str = f"SAVLIB LIB({library.strip()}) DEV(*SAVF) SAVF({toLibrary.strip()}/{saveFileName.strip()}) TGTRLS({version.strip()})"
+140            command_str += f" SAVF({toLibrary.strip()}/{saveFileName.strip()}) TGTRLS({version.strip()})"
+141            #print(command_str)
+142            try:
+143                with self.conn.cursor() as cursor:
+144                    # execute the Command for creating a Savefile.
+145                    cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str))
+146                    if getZip:
+147                        try:
+148                            remote_temp_savf_path = join(remPath, saveFileName.upper() + '.savf')
+149
+150                            destination_local_path = join(localPath, saveFileName.upper() + '.savf')
+151                            command_str = (
+152                                f"CPYTOSTMF FROMMBR('/QSYS.LIB/{toLibrary.upper().strip()}.LIB/{saveFileName.upper().strip()}.FILE') "
+153                                f"TOSTMF('{remote_temp_savf_path.strip()}') STMFOPT(*REPLACE)"
+154                            )
+155
+156                            # Execute the command on the remote system
+157                            cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str,))
+158
+159                            if self.__getSavFile(localFilePath=destination_local_path,
+160                                                 remotePath=remote_temp_savf_path, port=port):
+161                                rmvCommand = f"QSH CMD('rm -r {remote_temp_savf_path}')"
+162                                cursor.execute("CALL QSYS2.QCMDEXC(?)", (rmvCommand))
+163                            else:
+164                                raise ValueError("Something went wrong. With downloading the Save File.")
+165                            if remSavf:
+166                                if not self.removeFile(library=toLibrary, saveFileName=saveFileName):
+167                                    raise ValueError(f"The Save File {saveFileName} was not successfully removed.")
+168
+169                        except Exception as e:
+170                            self.__handle_error(error=e, pgm="saveLibrary - Transfer")
+171
+172            except Exception as e:
+173                self.__handle_error(error=e, pgm="saveLibrary")
+174                self.conn.rollback()
+175                return False
+176            else:
+177                self.conn.commit()
+178                if getZip:
+179                    print(f"File successfully downloaded to: {destination_local_path}")
+180                    return True
+181
+182                print(f"Successfully saved in the Library '{library}' successfully.")
+183                return True
+184
+185        return False
+186
+187    # ------------------------------------------------------
+188    # sub Function: create the Savefile on the AS400
+189    # ------------------------------------------------------
+190    def __crtsavf(self,
+191                  saveFileName: str,
+192                  library: str,
+193                  description: str = None,
+194                  max_records: Union[int, str, None] = None,
+195                  asp: Union[int, str, None] = None,
+196                  waitFile: Union[int, str, None] = None,
+197                  share: str = None,
+198                  authority: str = None
+199                  ) -> bool:
+200        """
+201            Sub-function to create a save file on the IBM i server.
+202
+203            This function executes the `CRTSAVF` (Create Save File) CL command
+204            to create a new save file in the specified library. This is a
+205            prerequisite for saving a library's contents.
+206
+207            Args:
+208                saveFileName (str): The name of the save file to be created.
+209                                    This will be the AS/400 object name.
+210                library (str): The name of the library where the save file will be created.
+211                description (str, optional): A text description for the save file. Defaults to None.
+212
+213            Returns:
+214                bool: True if the save file was created successfully, False otherwise.
+215        """
+216        # check is a parameter empty or not
+217
+218        if not saveFileName:
+219            raise ValueError("A file name is required.")
+220        if not library:
+221            raise ValueError("A library name is required.")
+222        if not description:
+223            description = 'A SaveFile from iLibrary'
+224
+225        command_str: str = f"CRTSAVF FILE({library.upper().strip()}/{saveFileName.upper().strip()}) TEXT('{description.strip()}')"
+226
+227        # check max_records for MAXRCDS parameter
+228        if self.__validate_max_value(value=max_records, param_name='max_records', str_format=['*NOMAX'],
+229                                     max_limit=4293525600) and not None:
+230            command_str += f" MAXRCDS({max_records})"
+231        # check asp for ASP 2147483647
+232        if self.__validate_max_value(value=asp, param_name='asp', str_format=['*LIBASP'], max_limit=32) and not None:
+233            command_str += f" ASP({asp})"
+234        if self.__validate_max_value(value=waitFile, param_name='waitFile', str_format=['*IMMED', '*CLS'],
+235                                     max_limit=2147483647) and not None:
+236            command_str += f" WAITFILE({waitFile})"
+237        if self.__validate_max_value(value=share, param_name='share', str_format=['*YES', '*NO']) and not None:
+238            command_str += f" SHARE({share})"
+239
+240        if authority is not None:
+241            upper_authority = authority.upper()
+242
+243            # 1. Check for custom authority (not in list AND up to 10 chars)
+244            if upper_authority not in ['*EXCLUDE', '*ALL', '*CHANGE', '*LIBCRTAUT', '*USE'] and len(
+245                    upper_authority) <= 10:
+246                # **CORRECTION 1: Use upper_authority here, not the undefined 'auth'**
+247                command_str += f" AUT({upper_authority})"
+248                # The 'pass' statements are redundant and can be removed
+249
+250            # 2. Add an 'elif' to handle the case where it IS one of the standard values
+251            elif upper_authority in ['*EXCLUDE', '*ALL', '*CHANGE', '*LIBCRTAUT', '*USE']:
+252                command_str += f" AUT({upper_authority})"
+253
+254
+255        try:
+256            with self.conn.cursor() as cursor:
+257                # execute the Command for creating a Savefile.
+258                cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str))
+259
+260        except Exception as e:
+261            self.__handle_error(error=e, pgm="__crtsavf")
+262            # remove a SAVF if its exists and we got an error
+263            if e.args[0] == 'HY000':
+264                sql = """
+265                      SELECT 1
+266                      FROM QSYS2.SAVE_FILE_INFO
+267                      WHERE SAVE_FILE_LIBRARY = ? \
+268                        AND SAVE_FILE = ?
+269                          FETCH FIRST 1 ROW ONLY \
+270                      """
+271                cursor = self.conn.cursor()
+272                cursor.execute(sql, library, saveFileName)
+273                result = cursor.fetchone()
+274                if result is not None:
+275                    self.removeFile(library=library, saveFileName=saveFileName)
+276            self.conn.rollback()
+277            raise ValueError(e)
+278        else:
+279            self.conn.commit()
+280            return True
+281
+282    # --------------------------------------------------------------------------
+283    # __validate_max_value - Helper Function for checking parameter
+284    # --------------------------------------------------------------------------
+285    def __validate_max_value(self,
+286                             value: Union[int, str, None],
+287                             param_name: str,
+288                             str_format: list[str],
+289                             min_limit: int = 1,
+290                             max_limit: int = None
+291                             ) -> Union[int, str, bool]:  # Includes bool as requested
+292        """
+293        Validates an input value for 'MAX' type parameters against a custom range.
+294        Handles special strings defined in str_format and numeric values.
+295
+296        Returns: The validated integer, the standardized special string, or False on failure (if no exception is raised).
+297        Raises: ValueError for invalid string format or out-of-range number.
+298        """
+299
+300        # Helper for clear error messages
+301        str_options = ", ".join([f"'{s}'" for s in str_format])
+302
+303        # 1. Handle special string
+304        if isinstance(value, str):
+305            upper_value = value.upper()
+306
+307            for special_value in str_format:
+308                normalized_special_value = special_value.upper()
+309
+310                if upper_value == special_value.upper() or upper_value == normalized_special_value:
+311                    # Found a match! Return the official, fully formatted string.
+312                    return special_value
+313
+314        # 2. Attempt Numeric Conversion (handles int and string-of-int)
+315        if value is not None:
+316            try:
+317                numeric_value = int(value)
+318            except ValueError:
+319                # Value is an invalid string (e.g., 'hello')
+320                raise ValueError(
+321                    f"Invalid value for {param_name}. Must be '{str_format}' or a number "
+322                    f"between {min_limit} and {max_limit:,}."
+323                )
+324        else:
+325            # If the value is None
+326            return False
+327
+328        # 3. Check Numeric Range
+329        if min_limit <= numeric_value <= max_limit:
+330            return numeric_value
+331        else:
+332            # Number is out of range
+333            raise ValueError(
+334                f"Invalid numeric value for {param_name}. Must be between {min_limit} and {max_limit:,}. "
+335                f"Received: {numeric_value}"
+336            )
+337
+338    # ------------------------------------------------------
+339    # getZipFile - getting the Zipfile from the SaveFile
+340    # ------------------------------------------------------
+341    def __getSavFile(self,
+342                     localFilePath: str,
+343                     remotePath: str,
+344                     port: int = None
+345                     ) -> bool:
+346        """
+347            Downloads a file from the remote IBM i via SFTP.
+348
+349            This method uses Paramiko to establish a secure shell (SSH) connection and
+350            then an SFTP session to transfer a file from a specified remote location
+351            on the IBM i's IFS to a local path.
+352
+353            Args:
+354                localFilePath (str): The full path to the file on the remote IBM i's IFS.
+355                remotePath (str): The full path on the local machine where the file
+356                                       will be saved. For example, '/Users/user/Documents/somefile.savf'.
+357                port (int, optional): The port to connect to the IBMi server. Defaults to None.
+358
+359            Returns:
+360                bool: True if the file was downloaded successfully, False otherwise.
+361
+362            Raises:
+363                ValueError: If either the remote_file_path or local_save_path is not provided.
+364        """
+365        if not localFilePath:
+366            print("Error: A local file path is required.")
+367            return False
+368        if not remotePath:
+369            print("Error: A remote path is required.")
+370            return False
+371        if not port:
+372            port = 2222
+373
+374        remotePath = PureWindowsPath(remotePath).as_posix()
+375        ssh_client = paramiko.SSHClient()
+376
+377        ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+378
+379        try:
+380            with ssh_client:
+381                ssh_client.connect(
+382                    hostname=self.db_host,
+383                    username=self.db_user,
+384                    password=self.db_password,
+385                    port=port
+386                )
+387                with ssh_client.open_sftp() as ftp_client:
+388                    ftp_client.get(remotePath, localFilePath)
+389                    return True
+390
+391        except paramiko.ssh_exception.AuthenticationException as e:
+392            print(f"Authentication failed. Check your username and password: {e}")
+393            return False
+394        except paramiko.ssh_exception.SSHException as e:
+395            print(f"SSH error occurred: {e}")
+396            return False
+397        except FileNotFoundError as e:
+398            print(f"File not found on the remote host: {e}")
+399            return False
+400
+401        finally:
+402            pass
+403
+404    def removeFile(self, library: str, saveFileName: str) -> bool:
+405        """
+406        Removes a save file from the specified library.
+407
+408        This function executes the system command to delete a save file from an IBM i
+409        system. It connects to the database through a cursor, and attempts to perform
+410        the operation. If an error is encountered during execution, the function
+411        rolls back the transaction and logs the error. On success, the transaction
+412        is committed.
+413
+414        :param library: The name of the library containing the save file to be removed.
+415        :type library: str
+416        :param saveFileName: The name of the save file to be removed.
+417        :type saveFileName: str
+418        :return: True if the save file is removed successfully, otherwise False.
+419        :rtype: bool
+420        """
+421        command_str: str = f"DLTF FILE({library.upper()}/{saveFileName.upper()})"
+422        try:
+423            with self.conn.cursor() as cursor:
+424                # execute the Command for deleting a Savefile.
+425                cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str))
+426
+427        except Exception as e:
+428            self.__handle_error(error=e, pgm="removeFile")
+429            self.conn.rollback()
+430            return False
+431        else:
+432            self.conn.commit()
+433            return True
+434
+435    def __handle_error(self, error, pgm: str):
+436        """
+437        Handles errors encountered during the execution of a command.
+438
+439        This method processes an error raised during the execution of a command in a
+440        specific function and extracts detailed error information including SQLSTATE
+441        and the error message. The formatted details are printed to the console for
+442        debugging purposes.
+443
+444        :param error: The error object encountered during command execution.
+445        :type error: Exception
+446        :param pgm: The name of the function where the error occurred.
+447        :type pgm: str
+448        :return: None
+449        """
+450        print("-------------------------------------------------------------")
+451        print(f"An error occurred while executing command in function {pgm}:")
+452        sqlstate = error.args[0]
+453        error_message = error.args[1]
+454
+455        print(f"SQLSTATE: {sqlstate}")
+456        print(f"Message: {error_message}")
+
+ + + + +
+ +
+ + def + saveLibrary( self, library: str, saveFileName: str, dev: str = None, vol: str = None, toLibrary: str = None, description: str = None, localPath: str = None, remPath: str = None, getZip: bool = False, port: int = None, remSavf=True, version: str = None, max_records: int | str | None = None, asp: int | str | None = None, waitFile: int | str | None = None, share: str = None, authority: str = None) -> bool: + + + +
+ +
 15    def saveLibrary(self,
+ 16                    library: str,
+ 17                    saveFileName: str,
+ 18                    dev: str = None,
+ 19                    vol: str = None,
+ 20                    toLibrary: str = None,
+ 21                    description: str = None,
+ 22                    localPath: str = None,
+ 23                    remPath: str = None,
+ 24                    getZip: bool = False,
+ 25                    port: int = None,
+ 26                    remSavf=True,
+ 27                    version: str = None,
+ 28                    max_records: Union[int, str, None] = None,
+ 29                    asp: Union[int, str, None] = None,
+ 30                    waitFile: Union[int, str, None] = None,
+ 31                    share: str = None,
+ 32                    authority: str = None
+ 33                    ) -> bool:
+ 34        """
+ 35        Saves a library to a specified save file, providing options for further customization such
+ 36        as setting the target release, saving as a zip file, specifying the device, volume, and more.
+ 37
+ 38        :param library: The name of the library to be saved. Must be a valid library name or one of
+ 39            the predefined options such as '*NONSYS', '*ALLUSR', '*IBM', etc.
+ 40        :type library: str
+ 41        :param saveFileName: The name of the save file where the library will be saved.
+ 42        :type saveFileName: str
+ 43        :param dev: The target device for the save operation. Defaults to '*SAVF' if not provided.
+ 44        :type dev: str, optional
+ 45        :param vol: Specifies the volume to be used. Use ‘*MOUNTED’ to refer to the mounted volume.
+ 46        :type vol: str, optional
+ 47        :param toLibrary: Target library where the save file will be temporarily stored. Defaults
+ 48            to the value of `library` if not specified.
+ 49        :type toLibrary: str, optional
+ 50        :param description: An optional description for the save file to be created.
+ 51        :type description: str, optional
+ 52        :param localPath: The local path where the save file will be downloaded if `getZip` is set
+ 53            to True. Must be an absolute path.
+ 54        :type localPath: str, optional
+ 55        :param remPath: The remote directory path on the target system to temporarily store the
+ 56            save file if `getZip` is set to True. Must be an absolute path.
+ 57        :type remPath: str, optional
+ 58        :param getZip: A flag that determines whether the save file should be archived into a zip
+ 59            file and downloaded locally.
+ 60        :type getZip: bool
+ 61        :param port: Specifies the port to be used for transferring the save file when `getZip` is
+ 62            enabled.
+ 63        :type port: int, optional
+ 64        :param remSavf: A flag indicating whether the save file should be removed from the remote
+ 65            target system after a successful save.
+ 66        :type remSavf: bool
+ 67        :param version: The target release version for the save operation. Valid values include
+ 68            ‘*CURRENT’, or specific OS versions like 'V1R1M0', 'V2R3M0', and so on.
+ 69        :type version: str, optional
+ 70        :param max_records: Optional parameter for specifying the maximum number of records in
+ 71            the save file.
+ 72        :type max_records: Union[int, str, None], optional
+ 73        :param asp: Auxiliary storage pool (ASP) device number or name if applicable.
+ 74        :type asp: Union[int, str, None], optional
+ 75        :param waitFile: The amount of time to wait for file access locks to be released.
+ 76        :type waitFile: Union[int, str, None], optional
+ 77        :param share: Specifies the share handling for threads or users accessing the save file.
+ 78        :type share: str, optional
+ 79        :param authority: Authority option to set for the save file being saved.
+ 80        :type authority: str, optional
+ 81        :return: A boolean indicating whether the library was successfully saved. Returns True on
+ 82            success or False on failure.
+ 83        :rtype: bool
+ 84        """
+ 85        # Target Release List
+ 86        trgList: list = ["V1R1M0", "V1R1M2", "V1R2M0", "V1R3M0", "V2R1M0", "V2R1M1",
+ 87                         "V2R2M0", "V2R3M0", "V3R0M5", "V3R1M0", "V3R2M0", "V3R6M0",
+ 88                         "V3R7M0", "V4R1M0", "V4R2M0", "V4R3M0", "V4R4M0", "V4R5M0",
+ 89                         "V5R1M0", "V5R2M0", "V5R3M0", "V5R4M0", "V6R1M0", "V6R1M1",
+ 90                         "V7R1M0", "V7R2M0", "V7R3M0", "V7R4M0", "V7R5M0", "V7R6M0"]
+ 91
+ 92        # check if something missing from the Arguments
+ 93        # check if Library is empty or not
+ 94        if not library:
+ 95            raise ValueError("A library name is required.")
+ 96        # check if saveFileName is empty or not
+ 97        if not saveFileName:
+ 98            raise ValueError("A save file name is required.")
+ 99        # check if toLibrary is empty or not
+100        if not toLibrary:
+101            toLibrary = library
+102        # check if user want the SaveFile as ZIP File
+103        if getZip:
+104            if not remPath:
+105                raise ValueError("A remote path is required. Use 'remPath' instead.")
+106            elif remPath[-1] == '/':
+107                remPath = remPath[:-1]
+108            if not localPath:
+109                raise ValueError("A local path is required. Use 'localPath' instead.")
+110            elif localPath[-1] == '/':
+111                localPath = localPath[:-1]
+112        # check wich Version of SaveFile is wanted
+113        if not version in list(trgList):
+114            version = "*CURRENT"
+115        else:
+116            version = version.upper()
+117        command_str: str = f'SAVLIB'
+118
+119        # check if Library is valid or not
+120        validated_library = self.__validate_max_value(value=library, param_name='library',
+121                                                      str_format=['*NONSYS', '*ALLUSR', '*IBM', '*SELECT', '*USRSPC',
+122                                                                  library])
+123        if validated_library:
+124            command_str += f' LIB({validated_library})'
+125        else:
+126            library_str = str(library)
+127            raise ValueError(
+128                f"The library '{library_str}' is not valid. Must be one of the specified strings or a valid number.")
+129        # check Dev - Device
+130        if not dev in ['*SAVF', '*MEDDFN']:
+131            command_str += f' DEV(*SAVF)'
+132        else:
+133            command_str += f' DEV({dev.upper()})'
+134        if vol is not None and vol == '*MOUNTED':
+135            command_str += f' VOL({vol})'
+136        # starting with mem main Sourcecode of saveLLibrary
+137        if self.__crtsavf(saveFileName, toLibrary, description, max_records=max_records, asp=asp, waitFile=waitFile,
+138                          share=share, authority=authority):
+139            # command_str: str = f"SAVLIB LIB({library.strip()}) DEV(*SAVF) SAVF({toLibrary.strip()}/{saveFileName.strip()}) TGTRLS({version.strip()})"
+140            command_str += f" SAVF({toLibrary.strip()}/{saveFileName.strip()}) TGTRLS({version.strip()})"
+141            #print(command_str)
+142            try:
+143                with self.conn.cursor() as cursor:
+144                    # execute the Command for creating a Savefile.
+145                    cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str))
+146                    if getZip:
+147                        try:
+148                            remote_temp_savf_path = join(remPath, saveFileName.upper() + '.savf')
+149
+150                            destination_local_path = join(localPath, saveFileName.upper() + '.savf')
+151                            command_str = (
+152                                f"CPYTOSTMF FROMMBR('/QSYS.LIB/{toLibrary.upper().strip()}.LIB/{saveFileName.upper().strip()}.FILE') "
+153                                f"TOSTMF('{remote_temp_savf_path.strip()}') STMFOPT(*REPLACE)"
+154                            )
+155
+156                            # Execute the command on the remote system
+157                            cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str,))
+158
+159                            if self.__getSavFile(localFilePath=destination_local_path,
+160                                                 remotePath=remote_temp_savf_path, port=port):
+161                                rmvCommand = f"QSH CMD('rm -r {remote_temp_savf_path}')"
+162                                cursor.execute("CALL QSYS2.QCMDEXC(?)", (rmvCommand))
+163                            else:
+164                                raise ValueError("Something went wrong. With downloading the Save File.")
+165                            if remSavf:
+166                                if not self.removeFile(library=toLibrary, saveFileName=saveFileName):
+167                                    raise ValueError(f"The Save File {saveFileName} was not successfully removed.")
+168
+169                        except Exception as e:
+170                            self.__handle_error(error=e, pgm="saveLibrary - Transfer")
+171
+172            except Exception as e:
+173                self.__handle_error(error=e, pgm="saveLibrary")
+174                self.conn.rollback()
+175                return False
+176            else:
+177                self.conn.commit()
+178                if getZip:
+179                    print(f"File successfully downloaded to: {destination_local_path}")
+180                    return True
+181
+182                print(f"Successfully saved in the Library '{library}' successfully.")
+183                return True
+184
+185        return False
+
+ + +

Saves a library to a specified save file, providing options for further customization such +as setting the target release, saving as a zip file, specifying the device, volume, and more.

+ +
Parameters
+ +
    +
  • library: The name of the library to be saved. Must be a valid library name or one of +the predefined options such as '*NONSYS', '*ALLUSR', '*IBM', etc.
  • +
  • saveFileName: The name of the save file where the library will be saved.
  • +
  • dev: The target device for the save operation. Defaults to '*SAVF' if not provided.
  • +
  • vol: Specifies the volume to be used. Use ‘*MOUNTED’ to refer to the mounted volume.
  • +
  • toLibrary: Target library where the save file will be temporarily stored. Defaults +to the value of library if not specified.
  • +
  • description: An optional description for the save file to be created.
  • +
  • localPath: The local path where the save file will be downloaded if getZip is set +to True. Must be an absolute path.
  • +
  • remPath: The remote directory path on the target system to temporarily store the +save file if getZip is set to True. Must be an absolute path.
  • +
  • getZip: A flag that determines whether the save file should be archived into a zip +file and downloaded locally.
  • +
  • port: Specifies the port to be used for transferring the save file when getZip is +enabled.
  • +
  • remSavf: A flag indicating whether the save file should be removed from the remote +target system after a successful save.
  • +
  • version: The target release version for the save operation. Valid values include +‘*CURRENT’, or specific OS versions like 'V1R1M0', 'V2R3M0', and so on.
  • +
  • max_records: Optional parameter for specifying the maximum number of records in +the save file.
  • +
  • asp: Auxiliary storage pool (ASP) device number or name if applicable.
  • +
  • waitFile: The amount of time to wait for file access locks to be released.
  • +
  • share: Specifies the share handling for threads or users accessing the save file.
  • +
  • authority: Authority option to set for the save file being saved.
  • +
+ +
Returns
+ +
+

A boolean indicating whether the library was successfully saved. Returns True on + success or False on failure.

+
+
+ + +
+
+ +
+ + def + removeFile(self, library: str, saveFileName: str) -> bool: + + + +
+ +
404    def removeFile(self, library: str, saveFileName: str) -> bool:
+405        """
+406        Removes a save file from the specified library.
+407
+408        This function executes the system command to delete a save file from an IBM i
+409        system. It connects to the database through a cursor, and attempts to perform
+410        the operation. If an error is encountered during execution, the function
+411        rolls back the transaction and logs the error. On success, the transaction
+412        is committed.
+413
+414        :param library: The name of the library containing the save file to be removed.
+415        :type library: str
+416        :param saveFileName: The name of the save file to be removed.
+417        :type saveFileName: str
+418        :return: True if the save file is removed successfully, otherwise False.
+419        :rtype: bool
+420        """
+421        command_str: str = f"DLTF FILE({library.upper()}/{saveFileName.upper()})"
+422        try:
+423            with self.conn.cursor() as cursor:
+424                # execute the Command for deleting a Savefile.
+425                cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str))
+426
+427        except Exception as e:
+428            self.__handle_error(error=e, pgm="removeFile")
+429            self.conn.rollback()
+430            return False
+431        else:
+432            self.conn.commit()
+433            return True
+
+ + +

Removes a save file from the specified library.

+ +

This function executes the system command to delete a save file from an IBM i +system. It connects to the database through a cursor, and attempts to perform +the operation. If an error is encountered during execution, the function +rolls back the transaction and logs the error. On success, the transaction +is committed.

+ +
Parameters
+ +
    +
  • library: The name of the library containing the save file to be removed.
  • +
  • saveFileName: The name of the save file to be removed.
  • +
+ +
Returns
+ +
+

True if the save file is removed successfully, otherwise False.

+
+
+ + +
+
+
+ + \ No newline at end of file diff --git a/docs/reference/iLibrary/src/sendMSG.md b/docs/reference/iLibrary/src/sendMSG.md new file mode 100644 index 0000000..3c4aaea --- /dev/null +++ b/docs/reference/iLibrary/src/sendMSG.md @@ -0,0 +1,497 @@ + + + + + + + iLibrary.src.sendMSG API documentation + + + + + + + + + +
+
+

+iLibrary.src.sendMSG

+ + + + + + +
 1from os.path import join
+ 2import paramiko
+ 3import pyodbc
+ 4import json
+ 5from datetime import datetime, date
+ 6from decimal import Decimal
+ 7
+ 8class sendMSG():
+ 9    """
+10    Handles message-related operations by providing functionality to send messages
+11    to specific users within the system. The class interacts with system APIs to
+12    execute the required operations and ensures the input parameters are validated
+13    before proceeding with the message sending process.
+14
+15    Attributes supported by this class are not specified because the class relies
+16    on method-level operations.
+17    """
+18    def send_message_to_user(
+19            self,
+20            username: str,
+21            message: str,
+22            # tomsgq: str = None,
+23            # msgtype: str = None,
+24            # rpymsgq: str = None,
+25            ccsid: int = None
+26    ):
+27        """
+28        Sends a message to a specified user on the system. This method interacts with the system
+29        to send a message by executing an SQL query. It validates the required
+30        inputs and raises an exception if they are missing. Optional parameters for
+31        further message configuration can also be provided.
+32
+33        :param username: The username of the recipient to whom the message will be sent.
+34        :type username: str
+35        :param message: The actual text message to be sent to the user.
+36        :type message: str
+37        :param ccsid: Optional character set identifier (CCSID) for the message. Defaults to None.
+38        :type ccsid: int, optional
+39
+40        :return: None if the message is sent successfully.
+41        :rtype: None
+42
+43        :raises ValueError: If any required parameter, such as `username` or `message`, is missing.
+44
+45        :raises Exception: Any other exceptions that occur during the execution of the
+46            SQL query are raised, indicating issues during the process of sending the
+47            message.
+48        """
+49        if not username:
+50            raise ValueError("Username are required.")
+51        if not message:
+52            raise ValueError("Message are required.")
+53
+54        username = username.upper()
+55        message = message.upper()
+56
+57        sql_query = f"CALL QSYS2.QCMDEXC('SNDMSG MSG(''{message}'') TOUSR({username})')"
+58        if ccsid:
+59            sql_query += f" CCSID({ccsid})"
+60        try:
+61            with self.conn.cursor() as cursor:
+62                cursor.execute(sql_query)
+63                row_dict:dict = {"success": f'Message sent to {username}'}
+64                return json.dumps(row_dict, indent=4)
+65        except Exception as e:
+66            row_dict: dict = {"error" : f'Error:  {e}'}
+67            return json.dumps(row_dict, indent=4)
+
+ + +
+
+ +
+ + class + sendMSG: + + + +
+ +
 9class sendMSG():
+10    """
+11    Handles message-related operations by providing functionality to send messages
+12    to specific users within the system. The class interacts with system APIs to
+13    execute the required operations and ensures the input parameters are validated
+14    before proceeding with the message sending process.
+15
+16    Attributes supported by this class are not specified because the class relies
+17    on method-level operations.
+18    """
+19    def send_message_to_user(
+20            self,
+21            username: str,
+22            message: str,
+23            # tomsgq: str = None,
+24            # msgtype: str = None,
+25            # rpymsgq: str = None,
+26            ccsid: int = None
+27    ):
+28        """
+29        Sends a message to a specified user on the system. This method interacts with the system
+30        to send a message by executing an SQL query. It validates the required
+31        inputs and raises an exception if they are missing. Optional parameters for
+32        further message configuration can also be provided.
+33
+34        :param username: The username of the recipient to whom the message will be sent.
+35        :type username: str
+36        :param message: The actual text message to be sent to the user.
+37        :type message: str
+38        :param ccsid: Optional character set identifier (CCSID) for the message. Defaults to None.
+39        :type ccsid: int, optional
+40
+41        :return: None if the message is sent successfully.
+42        :rtype: None
+43
+44        :raises ValueError: If any required parameter, such as `username` or `message`, is missing.
+45
+46        :raises Exception: Any other exceptions that occur during the execution of the
+47            SQL query are raised, indicating issues during the process of sending the
+48            message.
+49        """
+50        if not username:
+51            raise ValueError("Username are required.")
+52        if not message:
+53            raise ValueError("Message are required.")
+54
+55        username = username.upper()
+56        message = message.upper()
+57
+58        sql_query = f"CALL QSYS2.QCMDEXC('SNDMSG MSG(''{message}'') TOUSR({username})')"
+59        if ccsid:
+60            sql_query += f" CCSID({ccsid})"
+61        try:
+62            with self.conn.cursor() as cursor:
+63                cursor.execute(sql_query)
+64                row_dict:dict = {"success": f'Message sent to {username}'}
+65                return json.dumps(row_dict, indent=4)
+66        except Exception as e:
+67            row_dict: dict = {"error" : f'Error:  {e}'}
+68            return json.dumps(row_dict, indent=4)
+
+ + +

Handles message-related operations by providing functionality to send messages +to specific users within the system. The class interacts with system APIs to +execute the required operations and ensures the input parameters are validated +before proceeding with the message sending process.

+ +

Attributes supported by this class are not specified because the class relies +on method-level operations.

+
+ + +
+ +
+ + def + send_message_to_user(self, username: str, message: str, ccsid: int = None): + + + +
+ +
19    def send_message_to_user(
+20            self,
+21            username: str,
+22            message: str,
+23            # tomsgq: str = None,
+24            # msgtype: str = None,
+25            # rpymsgq: str = None,
+26            ccsid: int = None
+27    ):
+28        """
+29        Sends a message to a specified user on the system. This method interacts with the system
+30        to send a message by executing an SQL query. It validates the required
+31        inputs and raises an exception if they are missing. Optional parameters for
+32        further message configuration can also be provided.
+33
+34        :param username: The username of the recipient to whom the message will be sent.
+35        :type username: str
+36        :param message: The actual text message to be sent to the user.
+37        :type message: str
+38        :param ccsid: Optional character set identifier (CCSID) for the message. Defaults to None.
+39        :type ccsid: int, optional
+40
+41        :return: None if the message is sent successfully.
+42        :rtype: None
+43
+44        :raises ValueError: If any required parameter, such as `username` or `message`, is missing.
+45
+46        :raises Exception: Any other exceptions that occur during the execution of the
+47            SQL query are raised, indicating issues during the process of sending the
+48            message.
+49        """
+50        if not username:
+51            raise ValueError("Username are required.")
+52        if not message:
+53            raise ValueError("Message are required.")
+54
+55        username = username.upper()
+56        message = message.upper()
+57
+58        sql_query = f"CALL QSYS2.QCMDEXC('SNDMSG MSG(''{message}'') TOUSR({username})')"
+59        if ccsid:
+60            sql_query += f" CCSID({ccsid})"
+61        try:
+62            with self.conn.cursor() as cursor:
+63                cursor.execute(sql_query)
+64                row_dict:dict = {"success": f'Message sent to {username}'}
+65                return json.dumps(row_dict, indent=4)
+66        except Exception as e:
+67            row_dict: dict = {"error" : f'Error:  {e}'}
+68            return json.dumps(row_dict, indent=4)
+
+ + +

Sends a message to a specified user on the system. This method interacts with the system +to send a message by executing an SQL query. It validates the required +inputs and raises an exception if they are missing. Optional parameters for +further message configuration can also be provided.

+ +
Parameters
+ +
    +
  • username: The username of the recipient to whom the message will be sent.
  • +
  • message: The actual text message to be sent to the user.
  • +
  • ccsid: Optional character set identifier (CCSID) for the message. Defaults to None.
  • +
+ +
Returns
+ +
+

None if the message is sent successfully.

+
+ +
Raises
+ +
    +
  • ValueError: If any required parameter, such as username or message, is missing.

  • +
  • Exception: Any other exceptions that occur during the execution of the +SQL query are raised, indicating issues during the process of sending the +message.

  • +
+
+ + +
+
+
+ + \ No newline at end of file diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 0000000..defe329 --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docs/reference/search.js b/docs/reference/search.js new file mode 100644 index 0000000..0eca9e9 --- /dev/null +++ b/docs/reference/search.js @@ -0,0 +1,46 @@ +window.pdocSearch = (function(){ +/** elasticlunr - http://weixsong.github.io * Copyright (C) 2017 Oliver Nightingale * Copyright (C) 2017 Wei Song * MIT Licensed */!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o

\n"}, {"fullname": "iLibrary.src.Library", "modulename": "iLibrary.src.Library", "kind": "module", "doc": "

\n"}, {"fullname": "iLibrary.src.Library.Library", "modulename": "iLibrary.src.Library", "qualname": "Library", "kind": "class", "doc": "

A class to manage libraries and files on an IBM i system.

\n\n

It provides methods to connect to the system via pyodbc for SQL and\nparamiko for SFTP transfers.

\n", "bases": "iLibrary.src.getInfoForLibrary.getInfoForLibrary, iLibrary.src.saveLibrary.saveLibrary"}, {"fullname": "iLibrary.src.Library.Library.__init__", "modulename": "iLibrary.src.Library", "qualname": "Library.__init__", "kind": "function", "doc": "

Initializes the class attributes for a database connection.\nThe actual connection is established in the __enter__ method.

\n\n

Args:\n db_user (str): The user ID for the database connection.\n db_password (str): The password for the database user.\n db_host (str): The system/host name for the database connection.\n db_driver (str): The ODBC driver to be used.

\n", "signature": "(db_user: str, db_password: str, db_host: str, db_driver: str)"}, {"fullname": "iLibrary.src.Library.Library.db_user", "modulename": "iLibrary.src.Library", "qualname": "Library.db_user", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.Library.Library.db_host", "modulename": "iLibrary.src.Library", "qualname": "Library.db_host", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.Library.Library.db_driver", "modulename": "iLibrary.src.Library", "qualname": "Library.db_driver", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.Library.Library.db_password", "modulename": "iLibrary.src.Library", "qualname": "Library.db_password", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.Library.Library.iclose", "modulename": "iLibrary.src.Library", "qualname": "Library.iclose", "kind": "function", "doc": "

A helper method to close the connection, also useful for manual closure.

\n", "signature": "(self):", "funcdef": "def"}, {"fullname": "iLibrary.src.User", "modulename": "iLibrary.src.User", "kind": "module", "doc": "

\n"}, {"fullname": "iLibrary.src.User.User", "modulename": "iLibrary.src.User", "qualname": "User", "kind": "class", "doc": "

A class to manage User on IBMi System

\n\n

It provides methods to connect to the system via pyodbc for SQL and\nparamiko for SFTP transfers.

\n", "bases": "iLibrary.src.getUserInfoForUser.getUserInfoForUser, iLibrary.src.sendMSG.sendMSG"}, {"fullname": "iLibrary.src.User.User.__init__", "modulename": "iLibrary.src.User", "qualname": "User.__init__", "kind": "function", "doc": "

Initializes the class attributes for a database connection.\nThe actual connection is established in the __enter__ method.

\n\n

Args:\n db_user (str): The user ID for the database connection.\n db_password (str): The password for the database user.\n db_host (str): The system/host name for the database connection.\n db_driver (str): The ODBC driver to be used.

\n", "signature": "(db_user: str, db_password: str, db_host: str, db_driver: str)"}, {"fullname": "iLibrary.src.User.User.db_user", "modulename": "iLibrary.src.User", "qualname": "User.db_user", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.User.User.db_host", "modulename": "iLibrary.src.User", "qualname": "User.db_host", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.User.User.db_driver", "modulename": "iLibrary.src.User", "qualname": "User.db_driver", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.User.User.db_password", "modulename": "iLibrary.src.User", "qualname": "User.db_password", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.User.User.iclose", "modulename": "iLibrary.src.User", "qualname": "User.iclose", "kind": "function", "doc": "

A helper method to close the connection, also useful for manual closure.

\n", "signature": "(self):", "funcdef": "def"}, {"fullname": "iLibrary.src.getInfoForLibrary", "modulename": "iLibrary.src.getInfoForLibrary", "kind": "module", "doc": "

\n"}, {"fullname": "iLibrary.src.getInfoForLibrary.getInfoForLibrary", "modulename": "iLibrary.src.getInfoForLibrary", "qualname": "getInfoForLibrary", "kind": "class", "doc": "

\n"}, {"fullname": "iLibrary.src.getInfoForLibrary.getInfoForLibrary.__init__", "modulename": "iLibrary.src.getInfoForLibrary", "qualname": "getInfoForLibrary.__init__", "kind": "function", "doc": "

\n", "signature": "(connection)"}, {"fullname": "iLibrary.src.getInfoForLibrary.getInfoForLibrary.conn", "modulename": "iLibrary.src.getInfoForLibrary", "qualname": "getInfoForLibrary.conn", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.getInfoForLibrary.getInfoForLibrary.getLibraryInfo", "modulename": "iLibrary.src.getInfoForLibrary", "qualname": "getInfoForLibrary.getLibraryInfo", "kind": "function", "doc": "

\n", "signature": "(self, library: str, wantJson=True):", "funcdef": "def"}, {"fullname": "iLibrary.src.getInfoForLibrary.getInfoForLibrary.getFileInfo", "modulename": "iLibrary.src.getInfoForLibrary", "qualname": "getInfoForLibrary.getFileInfo", "kind": "function", "doc": "

\n", "signature": "(self, library: str, qFiles: bool = False) -> str:", "funcdef": "def"}, {"fullname": "iLibrary.src.getInfoForLibrary.getInfoForLibrary.getAllLibraries", "modulename": "iLibrary.src.getInfoForLibrary", "qualname": "getInfoForLibrary.getAllLibraries", "kind": "function", "doc": "

\n", "signature": "(self):", "funcdef": "def"}, {"fullname": "iLibrary.src.getUserInfoForUser", "modulename": "iLibrary.src.getUserInfoForUser", "kind": "module", "doc": "

\n"}, {"fullname": "iLibrary.src.getUserInfoForUser.getUserInfoForUser", "modulename": "iLibrary.src.getUserInfoForUser", "qualname": "getUserInfoForUser", "kind": "class", "doc": "

Handles user information retrieval and messaging functionalities.

\n\n

This class provides methods to interact with the database for retrieving user information\nand to send messages to specified users. It supports data retrieval in different formats\n(e.g., JSON or tuple), and it enables system messaging with configurable options.

\n\n

:ivar conn: Database connection object used for executing queries.

\n"}, {"fullname": "iLibrary.src.getUserInfoForUser.getUserInfoForUser.getAllUsers", "modulename": "iLibrary.src.getUserInfoForUser", "qualname": "getUserInfoForUser.getAllUsers", "kind": "function", "doc": "

Retrieves all user information from the database. Optionally returns the data in\nJSON format depending on the provided parameter.

\n\n

Retrieves a list of users stored in the database and can output the data either as\na list of tuples or in JSON format. The query fetches all fields available in the\nuser information database table and handles cases where no data is found.

\n\n
Parameters
\n\n
    \n
  • wantJson: Boolean flag to indicate whether the result should be returned\nin JSON format. If set to False, the result will be a list of tuples. Default\nis False.
  • \n
\n\n
Returns
\n\n
\n

The data fetched from the database. When wantJson is True, returns a\n JSON object as a string. Otherwise, returns a list of tuples.

\n
\n", "signature": "(self, wantJson: bool = False):", "funcdef": "def"}, {"fullname": "iLibrary.src.getUserInfoForUser.getUserInfoForUser.getSingleUserInformation", "modulename": "iLibrary.src.getUserInfoForUser", "qualname": "getUserInfoForUser.getSingleUserInformation", "kind": "function", "doc": "

Retrieves information about a specific user from the database based on their username. The function supports\nreturning data either as a JSON-formatted string or as a tuple with corresponding database fields.

\n\n
Parameters
\n\n
    \n
  • username: The username of the database user whose information is to be retrieved. Must not be empty.
  • \n
  • wantJson: Indicates whether the output should be formatted as JSON. Defaults to False.
  • \n
\n\n
Returns
\n\n
\n

A tuple containing database fields if wantJson is False, or a JSON-formatted string if wantJson is True.\n If no user is found, returns either a JSON-formatted error message or a tuple with error details, based on the\n value of wantJson. Returns None if an exception occurs.

\n
\n\n
Raises
\n\n
    \n
  • ValueError: If the username input is empty or None.
  • \n
\n", "signature": "(self, username: str, wantJson: bool = False):", "funcdef": "def"}, {"fullname": "iLibrary.src.saveLibrary", "modulename": "iLibrary.src.saveLibrary", "kind": "module", "doc": "

\n"}, {"fullname": "iLibrary.src.saveLibrary.saveLibrary", "modulename": "iLibrary.src.saveLibrary", "qualname": "saveLibrary", "kind": "class", "doc": "

\n"}, {"fullname": "iLibrary.src.saveLibrary.saveLibrary.saveLibrary", "modulename": "iLibrary.src.saveLibrary", "qualname": "saveLibrary.saveLibrary", "kind": "function", "doc": "

Saves a library to a specified save file, providing options for further customization such\nas setting the target release, saving as a zip file, specifying the device, volume, and more.

\n\n
Parameters
\n\n
    \n
  • library: The name of the library to be saved. Must be a valid library name or one of\nthe predefined options such as '*NONSYS', '*ALLUSR', '*IBM', etc.
  • \n
  • saveFileName: The name of the save file where the library will be saved.
  • \n
  • dev: The target device for the save operation. Defaults to '*SAVF' if not provided.
  • \n
  • vol: Specifies the volume to be used. Use \u2018*MOUNTED\u2019 to refer to the mounted volume.
  • \n
  • toLibrary: Target library where the save file will be temporarily stored. Defaults\nto the value of library if not specified.
  • \n
  • description: An optional description for the save file to be created.
  • \n
  • localPath: The local path where the save file will be downloaded if getZip is set\nto True. Must be an absolute path.
  • \n
  • remPath: The remote directory path on the target system to temporarily store the\nsave file if getZip is set to True. Must be an absolute path.
  • \n
  • getZip: A flag that determines whether the save file should be archived into a zip\nfile and downloaded locally.
  • \n
  • port: Specifies the port to be used for transferring the save file when getZip is\nenabled.
  • \n
  • remSavf: A flag indicating whether the save file should be removed from the remote\ntarget system after a successful save.
  • \n
  • version: The target release version for the save operation. Valid values include\n\u2018*CURRENT\u2019, or specific OS versions like 'V1R1M0', 'V2R3M0', and so on.
  • \n
  • max_records: Optional parameter for specifying the maximum number of records in\nthe save file.
  • \n
  • asp: Auxiliary storage pool (ASP) device number or name if applicable.
  • \n
  • waitFile: The amount of time to wait for file access locks to be released.
  • \n
  • share: Specifies the share handling for threads or users accessing the save file.
  • \n
  • authority: Authority option to set for the save file being saved.
  • \n
\n\n
Returns
\n\n
\n

A boolean indicating whether the library was successfully saved. Returns True on\n success or False on failure.

\n
\n", "signature": "(\tself,\tlibrary: str,\tsaveFileName: str,\tdev: str = None,\tvol: str = None,\ttoLibrary: str = None,\tdescription: str = None,\tlocalPath: str = None,\tremPath: str = None,\tgetZip: bool = False,\tport: int = None,\tremSavf=True,\tversion: str = None,\tmax_records: int | str | None = None,\tasp: int | str | None = None,\twaitFile: int | str | None = None,\tshare: str = None,\tauthority: str = None) -> bool:", "funcdef": "def"}, {"fullname": "iLibrary.src.saveLibrary.saveLibrary.removeFile", "modulename": "iLibrary.src.saveLibrary", "qualname": "saveLibrary.removeFile", "kind": "function", "doc": "

Removes a save file from the specified library.

\n\n

This function executes the system command to delete a save file from an IBM i\nsystem. It connects to the database through a cursor, and attempts to perform\nthe operation. If an error is encountered during execution, the function\nrolls back the transaction and logs the error. On success, the transaction\nis committed.

\n\n
Parameters
\n\n
    \n
  • library: The name of the library containing the save file to be removed.
  • \n
  • saveFileName: The name of the save file to be removed.
  • \n
\n\n
Returns
\n\n
\n

True if the save file is removed successfully, otherwise False.

\n
\n", "signature": "(self, library: str, saveFileName: str) -> bool:", "funcdef": "def"}, {"fullname": "iLibrary.src.sendMSG", "modulename": "iLibrary.src.sendMSG", "kind": "module", "doc": "

\n"}, {"fullname": "iLibrary.src.sendMSG.sendMSG", "modulename": "iLibrary.src.sendMSG", "qualname": "sendMSG", "kind": "class", "doc": "

Handles message-related operations by providing functionality to send messages\nto specific users within the system. The class interacts with system APIs to\nexecute the required operations and ensures the input parameters are validated\nbefore proceeding with the message sending process.

\n\n

Attributes supported by this class are not specified because the class relies\non method-level operations.

\n"}, {"fullname": "iLibrary.src.sendMSG.sendMSG.send_message_to_user", "modulename": "iLibrary.src.sendMSG", "qualname": "sendMSG.send_message_to_user", "kind": "function", "doc": "

Sends a message to a specified user on the system. This method interacts with the system\nto send a message by executing an SQL query. It validates the required\ninputs and raises an exception if they are missing. Optional parameters for\nfurther message configuration can also be provided.

\n\n
Parameters
\n\n
    \n
  • username: The username of the recipient to whom the message will be sent.
  • \n
  • message: The actual text message to be sent to the user.
  • \n
  • ccsid: Optional character set identifier (CCSID) for the message. Defaults to None.
  • \n
\n\n
Returns
\n\n
\n

None if the message is sent successfully.

\n
\n\n
Raises
\n\n
    \n
  • ValueError: If any required parameter, such as username or message, is missing.

  • \n
  • Exception: Any other exceptions that occur during the execution of the\nSQL query are raised, indicating issues during the process of sending the\nmessage.

  • \n
\n", "signature": "(self, username: str, message: str, ccsid: int = None):", "funcdef": "def"}]; + + // mirrored in build-search-index.js (part 1) + // Also split on html tags. this is a cheap heuristic, but good enough. + elasticlunr.tokenizer.setSeperator(/[\s\-.;&_'"=,()]+|<[^>]*>/); + + let searchIndex; + if (docs._isPrebuiltIndex) { + console.info("using precompiled search index"); + searchIndex = elasticlunr.Index.load(docs); + } else { + console.time("building search index"); + // mirrored in build-search-index.js (part 2) + searchIndex = elasticlunr(function () { + this.pipeline.remove(elasticlunr.stemmer); + this.pipeline.remove(elasticlunr.stopWordFilter); + this.addField("qualname"); + this.addField("fullname"); + this.addField("annotation"); + this.addField("default_value"); + this.addField("signature"); + this.addField("bases"); + this.addField("doc"); + this.setRef("fullname"); + }); + for (let doc of docs) { + searchIndex.addDoc(doc); + } + console.timeEnd("building search index"); + } + + return (term) => searchIndex.search(term, { + fields: { + qualname: {boost: 4}, + fullname: {boost: 2}, + annotation: {boost: 2}, + default_value: {boost: 2}, + signature: {boost: 2}, + bases: {boost: 2}, + doc: {boost: 1}, + }, + expand: true + }); +})(); \ No newline at end of file From 0a35b6a784511904abd371729a9790d6d78a3de6 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:53:03 +0100 Subject: [PATCH 21/31] fixxes Docs Test --- docs/reference/iLibrary/src.md | 242 --- docs/reference/iLibrary/src/Library.md | 563 ------- docs/reference/iLibrary/src/User.md | 556 ------- .../iLibrary/src/getInfoForLibrary.md | 604 ------- .../iLibrary/src/getUserInfoForUser.md | 659 -------- docs/reference/iLibrary/src/saveLibrary.md | 1466 ----------------- docs/reference/iLibrary/src/sendMSG.md | 497 ------ docs/reference/index.md | 7 - docs/reference/search.js | 46 - 9 files changed, 4640 deletions(-) delete mode 100644 docs/reference/iLibrary/src.md delete mode 100644 docs/reference/iLibrary/src/Library.md delete mode 100644 docs/reference/iLibrary/src/User.md delete mode 100644 docs/reference/iLibrary/src/getInfoForLibrary.md delete mode 100644 docs/reference/iLibrary/src/getUserInfoForUser.md delete mode 100644 docs/reference/iLibrary/src/saveLibrary.md delete mode 100644 docs/reference/iLibrary/src/sendMSG.md delete mode 100644 docs/reference/index.md delete mode 100644 docs/reference/search.js diff --git a/docs/reference/iLibrary/src.md b/docs/reference/iLibrary/src.md deleted file mode 100644 index e98eb43..0000000 --- a/docs/reference/iLibrary/src.md +++ /dev/null @@ -1,242 +0,0 @@ - - - - - - - iLibrary.src API documentation - - - - - - - - - -
-
-

-iLibrary.src

- - - - - - -
1
-
- - -
-
- - \ No newline at end of file diff --git a/docs/reference/iLibrary/src/Library.md b/docs/reference/iLibrary/src/Library.md deleted file mode 100644 index 258cebe..0000000 --- a/docs/reference/iLibrary/src/Library.md +++ /dev/null @@ -1,563 +0,0 @@ - - - - - - - iLibrary.src.Library API documentation - - - - - - - - - -
-
-

-iLibrary.src.Library

- - - - - - -
 1from os.path import join
- 2import paramiko
- 3import pyodbc
- 4import json
- 5from datetime import datetime, date
- 6from decimal import Decimal
- 7from .getInfoForLibrary import *
- 8from .saveLibrary import *
- 9
-10
-11
-12class Library(getInfoForLibrary, saveLibrary):
-13    """
-14    A class to manage libraries and files on an IBM i system.
-15
-16    It provides methods to connect to the system via pyodbc for SQL and
-17    paramiko for SFTP transfers.
-18    """
-19
-20    # ------------------------------------------------------
-21    # __init__ - initzialise the class
-22    # ------------------------------------------------------
-23    def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str):
-24        """
-25        Initializes the class attributes for a database connection.
-26        The actual connection is established in the __enter__ method.
-27
-28        Args:
-29            db_user (str): The user ID for the database connection.
-30            db_password (str): The password for the database user.
-31            db_host (str): The system/host name for the database connection.
-32            db_driver (str): The ODBC driver to be used.
-33        """
-34        self.db_user = db_user
-35        self.db_host = db_host
-36        self.db_driver = db_driver
-37        self.db_password = db_password
-38
-39    # ------------------------------------------------------
-40    # __enter__ - enter to the class
-41    # ------------------------------------------------------
-42    def __enter__(self) -> 'Library':
-43        """
-44        Establishes the database connection when entering a 'with' block.
-45        """
-46        try:
-47            conn_str = (
-48                f"DRIVER={self.db_driver};"
-49                f"SYSTEM={self.db_host};"
-50                f"UID={self.db_user};"
-51                f"PWD={self.db_password};"
-52            )
-53            self.conn = pyodbc.connect(conn_str, autocommit=True)
-54            return self
-55        except pyodbc.Error as ex:
-56            sqlstate = ex.args[0]
-57            print(f"Database connection failed with error: {sqlstate}")
-58            raise
-59
-60    # ------------------------------------------------------
-61    # __exit__ - leave the class
-62    # ------------------------------------------------------
-63    def __exit__(self, exc_type, exc_val, exc_tb):
-64        """
-65        Closes the database connection when exiting a 'with' block.
-66        This method is called automatically, even if an error occurred.
-67        """
-68        self.iclose()
-69
-70
-71    # ------------------------------------------------------
-72    # iClose - close connection
-73    # ------------------------------------------------------
-74    def iclose(self):
-75        """
-76        A helper method to close the connection, also useful for manual closure.
-77        """
-78        if self.conn and not self.conn.closed:
-79            self.conn.close()
-80            pass
-
- - -
-
- - - -
13class Library(getInfoForLibrary, saveLibrary):
-14    """
-15    A class to manage libraries and files on an IBM i system.
-16
-17    It provides methods to connect to the system via pyodbc for SQL and
-18    paramiko for SFTP transfers.
-19    """
-20
-21    # ------------------------------------------------------
-22    # __init__ - initzialise the class
-23    # ------------------------------------------------------
-24    def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str):
-25        """
-26        Initializes the class attributes for a database connection.
-27        The actual connection is established in the __enter__ method.
-28
-29        Args:
-30            db_user (str): The user ID for the database connection.
-31            db_password (str): The password for the database user.
-32            db_host (str): The system/host name for the database connection.
-33            db_driver (str): The ODBC driver to be used.
-34        """
-35        self.db_user = db_user
-36        self.db_host = db_host
-37        self.db_driver = db_driver
-38        self.db_password = db_password
-39
-40    # ------------------------------------------------------
-41    # __enter__ - enter to the class
-42    # ------------------------------------------------------
-43    def __enter__(self) -> 'Library':
-44        """
-45        Establishes the database connection when entering a 'with' block.
-46        """
-47        try:
-48            conn_str = (
-49                f"DRIVER={self.db_driver};"
-50                f"SYSTEM={self.db_host};"
-51                f"UID={self.db_user};"
-52                f"PWD={self.db_password};"
-53            )
-54            self.conn = pyodbc.connect(conn_str, autocommit=True)
-55            return self
-56        except pyodbc.Error as ex:
-57            sqlstate = ex.args[0]
-58            print(f"Database connection failed with error: {sqlstate}")
-59            raise
-60
-61    # ------------------------------------------------------
-62    # __exit__ - leave the class
-63    # ------------------------------------------------------
-64    def __exit__(self, exc_type, exc_val, exc_tb):
-65        """
-66        Closes the database connection when exiting a 'with' block.
-67        This method is called automatically, even if an error occurred.
-68        """
-69        self.iclose()
-70
-71
-72    # ------------------------------------------------------
-73    # iClose - close connection
-74    # ------------------------------------------------------
-75    def iclose(self):
-76        """
-77        A helper method to close the connection, also useful for manual closure.
-78        """
-79        if self.conn and not self.conn.closed:
-80            self.conn.close()
-81            pass
-
- - -

A class to manage libraries and files on an IBM i system.

- -

It provides methods to connect to the system via pyodbc for SQL and -paramiko for SFTP transfers.

-
- - -
- -
- - Library(db_user: str, db_password: str, db_host: str, db_driver: str) - - - -
- -
24    def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str):
-25        """
-26        Initializes the class attributes for a database connection.
-27        The actual connection is established in the __enter__ method.
-28
-29        Args:
-30            db_user (str): The user ID for the database connection.
-31            db_password (str): The password for the database user.
-32            db_host (str): The system/host name for the database connection.
-33            db_driver (str): The ODBC driver to be used.
-34        """
-35        self.db_user = db_user
-36        self.db_host = db_host
-37        self.db_driver = db_driver
-38        self.db_password = db_password
-
- - -

Initializes the class attributes for a database connection. -The actual connection is established in the __enter__ method.

- -

Args: - db_user (str): The user ID for the database connection. - db_password (str): The password for the database user. - db_host (str): The system/host name for the database connection. - db_driver (str): The ODBC driver to be used.

-
- - -
-
-
- db_user - - -
- - - - -
-
-
- db_host - - -
- - - - -
-
-
- db_driver - - -
- - - - -
-
-
- db_password - - -
- - - - -
-
- -
- - def - iclose(self): - - - -
- -
75    def iclose(self):
-76        """
-77        A helper method to close the connection, also useful for manual closure.
-78        """
-79        if self.conn and not self.conn.closed:
-80            self.conn.close()
-81            pass
-
- - -

A helper method to close the connection, also useful for manual closure.

-
- - -
- -
-
- - \ No newline at end of file diff --git a/docs/reference/iLibrary/src/User.md b/docs/reference/iLibrary/src/User.md deleted file mode 100644 index b90738e..0000000 --- a/docs/reference/iLibrary/src/User.md +++ /dev/null @@ -1,556 +0,0 @@ - - - - - - - iLibrary.src.User API documentation - - - - - - - - - -
-
-

-iLibrary.src.User

- - - - - - -
 1from os.path import join
- 2import paramiko
- 3import pyodbc
- 4import json
- 5from datetime import datetime, date
- 6from decimal import Decimal
- 7from .getUserInfoForUser import *
- 8from .sendMSG import *
- 9
-10class User(getUserInfoForUser, sendMSG):
-11    """
-12        A class to manage User on IBMi System
-13
-14        It provides methods to connect to the system via pyodbc for SQL and
-15        paramiko for SFTP transfers.
-16    """
-17
-18    # ------------------------------------------------------
-19    # __init__ - initzialise the class
-20    # ------------------------------------------------------
-21    def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str):
-22        """
-23        Initializes the class attributes for a database connection.
-24        The actual connection is established in the __enter__ method.
-25
-26        Args:
-27            db_user (str): The user ID for the database connection.
-28            db_password (str): The password for the database user.
-29            db_host (str): The system/host name for the database connection.
-30            db_driver (str): The ODBC driver to be used.
-31        """
-32        self.db_user = db_user
-33        self.db_host = db_host
-34        self.db_driver = db_driver
-35        self.db_password = db_password
-36
-37    # ------------------------------------------------------
-38    # __enter__ - enter to the class
-39    # ------------------------------------------------------
-40    def __enter__(self) -> 'User':
-41        """
-42        Establishes the database connection when entering a 'with' block.
-43        """
-44        try:
-45            conn_str = (
-46                f"DRIVER={self.db_driver};"
-47                f"SYSTEM={self.db_host};"
-48                f"UID={self.db_user};"
-49                f"PWD={self.db_password};"
-50            )
-51            self.conn = pyodbc.connect(conn_str, autocommit=True)
-52            return self
-53        except pyodbc.Error as ex:
-54            sqlstate = ex.args[0]
-55            print(f"Database connection failed with error: {sqlstate}")
-56            raise
-57
-58    # ------------------------------------------------------
-59    # __exit__ - leave the class
-60    # ------------------------------------------------------
-61    def __exit__(self, exc_type, exc_val, exc_tb):
-62        """
-63        Closes the database connection when exiting a 'with' block.
-64        This method is called automatically, even if an error occurred.
-65        """
-66        self.iclose()
-67
-68    # ------------------------------------------------------
-69    # iClose - close connection
-70    # ------------------------------------------------------
-71    def iclose(self):
-72        """
-73        A helper method to close the connection, also useful for manual closure.
-74        """
-75        if self.conn and not self.conn.closed:
-76            self.conn.close()
-77            pass
-
- - -
-
- - - -
11class User(getUserInfoForUser, sendMSG):
-12    """
-13        A class to manage User on IBMi System
-14
-15        It provides methods to connect to the system via pyodbc for SQL and
-16        paramiko for SFTP transfers.
-17    """
-18
-19    # ------------------------------------------------------
-20    # __init__ - initzialise the class
-21    # ------------------------------------------------------
-22    def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str):
-23        """
-24        Initializes the class attributes for a database connection.
-25        The actual connection is established in the __enter__ method.
-26
-27        Args:
-28            db_user (str): The user ID for the database connection.
-29            db_password (str): The password for the database user.
-30            db_host (str): The system/host name for the database connection.
-31            db_driver (str): The ODBC driver to be used.
-32        """
-33        self.db_user = db_user
-34        self.db_host = db_host
-35        self.db_driver = db_driver
-36        self.db_password = db_password
-37
-38    # ------------------------------------------------------
-39    # __enter__ - enter to the class
-40    # ------------------------------------------------------
-41    def __enter__(self) -> 'User':
-42        """
-43        Establishes the database connection when entering a 'with' block.
-44        """
-45        try:
-46            conn_str = (
-47                f"DRIVER={self.db_driver};"
-48                f"SYSTEM={self.db_host};"
-49                f"UID={self.db_user};"
-50                f"PWD={self.db_password};"
-51            )
-52            self.conn = pyodbc.connect(conn_str, autocommit=True)
-53            return self
-54        except pyodbc.Error as ex:
-55            sqlstate = ex.args[0]
-56            print(f"Database connection failed with error: {sqlstate}")
-57            raise
-58
-59    # ------------------------------------------------------
-60    # __exit__ - leave the class
-61    # ------------------------------------------------------
-62    def __exit__(self, exc_type, exc_val, exc_tb):
-63        """
-64        Closes the database connection when exiting a 'with' block.
-65        This method is called automatically, even if an error occurred.
-66        """
-67        self.iclose()
-68
-69    # ------------------------------------------------------
-70    # iClose - close connection
-71    # ------------------------------------------------------
-72    def iclose(self):
-73        """
-74        A helper method to close the connection, also useful for manual closure.
-75        """
-76        if self.conn and not self.conn.closed:
-77            self.conn.close()
-78            pass
-
- - -

A class to manage User on IBMi System

- -

It provides methods to connect to the system via pyodbc for SQL and -paramiko for SFTP transfers.

-
- - -
- -
- - User(db_user: str, db_password: str, db_host: str, db_driver: str) - - - -
- -
22    def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str):
-23        """
-24        Initializes the class attributes for a database connection.
-25        The actual connection is established in the __enter__ method.
-26
-27        Args:
-28            db_user (str): The user ID for the database connection.
-29            db_password (str): The password for the database user.
-30            db_host (str): The system/host name for the database connection.
-31            db_driver (str): The ODBC driver to be used.
-32        """
-33        self.db_user = db_user
-34        self.db_host = db_host
-35        self.db_driver = db_driver
-36        self.db_password = db_password
-
- - -

Initializes the class attributes for a database connection. -The actual connection is established in the __enter__ method.

- -

Args: - db_user (str): The user ID for the database connection. - db_password (str): The password for the database user. - db_host (str): The system/host name for the database connection. - db_driver (str): The ODBC driver to be used.

-
- - -
-
-
- db_user - - -
- - - - -
-
-
- db_host - - -
- - - - -
-
-
- db_driver - - -
- - - - -
-
-
- db_password - - -
- - - - -
-
- -
- - def - iclose(self): - - - -
- -
72    def iclose(self):
-73        """
-74        A helper method to close the connection, also useful for manual closure.
-75        """
-76        if self.conn and not self.conn.closed:
-77            self.conn.close()
-78            pass
-
- - -

A helper method to close the connection, also useful for manual closure.

-
- - -
- -
-
- - \ No newline at end of file diff --git a/docs/reference/iLibrary/src/getInfoForLibrary.md b/docs/reference/iLibrary/src/getInfoForLibrary.md deleted file mode 100644 index 209812a..0000000 --- a/docs/reference/iLibrary/src/getInfoForLibrary.md +++ /dev/null @@ -1,604 +0,0 @@ - - - - - - - iLibrary.src.getInfoForLibrary API documentation - - - - - - - - - -
-
-

-iLibrary.src.getInfoForLibrary

- - - - - - -
 1import json
- 2from datetime import datetime, date
- 3from decimal import Decimal
- 4
- 5
- 6class getInfoForLibrary:
- 7    def __init__(self, connection):
- 8        self.conn = connection
- 9
-10    def _convert_to_json_ready(self, row, description):
-11        """Interne Hilfsmethode zur Typ-Konvertierung und Bereinigung."""
-12        row_dict = {}
-13        titles = [col[0] for col in description]
-14
-15        for i, value in enumerate(row):
-16            key = titles[i]
-17            # Typ-Prüfung für JSON-Serialisierung
-18            if isinstance(value, (datetime, date)):
-19                row_dict[key] = value.isoformat()
-20            elif isinstance(value, Decimal):
-21                row_dict[key] = float(value)
-22            elif isinstance(value, bytes):
-23                row_dict[key] = value.decode('utf-8', errors='replace')
-24            elif value is None:
-25                row_dict[key] = None
-26            else:
-27                # Entfernt unnötige Leerzeichen von CHAR-Feldern
-28                row_dict[key] = str(value).strip()
-29        return row_dict
-30
-31    def getLibraryInfo(self, library: str, wantJson=True):
-32        if not library or len(library) > 10:
-33            raise ValueError("Ungültiger Bibliotheksname (max. 10 Zeichen).")
-34
-35        sql_query = f"SELECT * FROM TABLE(QSYS2.LIBRARY_INFO(upper('{library}')))"
-36        try:
-37            with self.conn.cursor() as cursor:
-38                cursor.execute(sql_query)
-39                row = cursor.fetchone()
-40
-41                if not row:
-42                    error_msg = {"error": f"No data found for library: {library}"}
-43                    return json.dumps(error_msg, indent=4) if wantJson else ("error", error_msg["error"])
-44
-45                if wantJson:
-46                    return json.dumps(self._convert_to_json_ready(row, cursor.description), indent=4)
-47
-48                return row
-49        except Exception as e:
-50            print(f"Fehler bei getLibraryInfo: {e}")
-51            return None
-52
-53    def getFileInfo(self, library: str, qFiles: bool = False) -> str:
-54        if not library:
-55            return json.dumps([{"error": "A library name is required."}])
-56
-57        if qFiles:
-58            sql = f"SELECT * FROM QSYS2.SYSMEMBERSTAT WHERE SYSTEM_TABLE_SCHEMA = '{library.upper()}' AND SOURCE_TYPE IS NOT NULL ORDER BY SYSTEM_TABLE_MEMBER"
-59        else:
-60            sql = f"SELECT * FROM TABLE(QSYS2.OBJECT_STATISTICS('{library.upper()}', '*ALL')) AS X"
-61
-62        try:
-63            with self.conn.cursor() as cursor:
-64                cursor.execute(sql)
-65                rows = cursor.fetchall()
-66
-67                if not rows:
-68                    return json.dumps([{"error": f"No Files Found in Library: {library}"}])
-69
-70                result_list = [self._convert_to_json_ready(row, cursor.description) for row in rows]
-71                self.conn.commit()
-72                return json.dumps(result_list, indent=4)
-73        except Exception as e:
-74            if self.conn: self.conn.rollback()
-75            return json.dumps([{"error": f"Database Error: {str(e)}"}])
-76
-77    def getAllLibraries(self):
-78        # Hier nutzen wir nun auch die dynamische Spaltenerkennung statt der harten Liste
-79        sql = "SELECT * FROM TABLE(QSYS2.OBJECT_STATISTICS('*ALL', '*LIB')) AS X"
-80        try:
-81            with self.conn.cursor() as cursor:
-82                cursor.execute(sql)
-83                rows = cursor.fetchall()
-84
-85                if not rows:
-86                    return json.dumps([{"error": "No Libraries found"}])
-87
-88                result_list = [self._convert_to_json_ready(row, cursor.description) for row in rows]
-89                self.conn.commit()
-90                return json.dumps(result_list, indent=4)
-91        except Exception as e:
-92            print(f"Fehler bei getAllLibraries: {e}")
-93            if self.conn: self.conn.rollback()
-94            return False
-
- - -
-
- -
- - class - getInfoForLibrary: - - - -
- -
 7class getInfoForLibrary:
- 8    def __init__(self, connection):
- 9        self.conn = connection
-10
-11    def _convert_to_json_ready(self, row, description):
-12        """Interne Hilfsmethode zur Typ-Konvertierung und Bereinigung."""
-13        row_dict = {}
-14        titles = [col[0] for col in description]
-15
-16        for i, value in enumerate(row):
-17            key = titles[i]
-18            # Typ-Prüfung für JSON-Serialisierung
-19            if isinstance(value, (datetime, date)):
-20                row_dict[key] = value.isoformat()
-21            elif isinstance(value, Decimal):
-22                row_dict[key] = float(value)
-23            elif isinstance(value, bytes):
-24                row_dict[key] = value.decode('utf-8', errors='replace')
-25            elif value is None:
-26                row_dict[key] = None
-27            else:
-28                # Entfernt unnötige Leerzeichen von CHAR-Feldern
-29                row_dict[key] = str(value).strip()
-30        return row_dict
-31
-32    def getLibraryInfo(self, library: str, wantJson=True):
-33        if not library or len(library) > 10:
-34            raise ValueError("Ungültiger Bibliotheksname (max. 10 Zeichen).")
-35
-36        sql_query = f"SELECT * FROM TABLE(QSYS2.LIBRARY_INFO(upper('{library}')))"
-37        try:
-38            with self.conn.cursor() as cursor:
-39                cursor.execute(sql_query)
-40                row = cursor.fetchone()
-41
-42                if not row:
-43                    error_msg = {"error": f"No data found for library: {library}"}
-44                    return json.dumps(error_msg, indent=4) if wantJson else ("error", error_msg["error"])
-45
-46                if wantJson:
-47                    return json.dumps(self._convert_to_json_ready(row, cursor.description), indent=4)
-48
-49                return row
-50        except Exception as e:
-51            print(f"Fehler bei getLibraryInfo: {e}")
-52            return None
-53
-54    def getFileInfo(self, library: str, qFiles: bool = False) -> str:
-55        if not library:
-56            return json.dumps([{"error": "A library name is required."}])
-57
-58        if qFiles:
-59            sql = f"SELECT * FROM QSYS2.SYSMEMBERSTAT WHERE SYSTEM_TABLE_SCHEMA = '{library.upper()}' AND SOURCE_TYPE IS NOT NULL ORDER BY SYSTEM_TABLE_MEMBER"
-60        else:
-61            sql = f"SELECT * FROM TABLE(QSYS2.OBJECT_STATISTICS('{library.upper()}', '*ALL')) AS X"
-62
-63        try:
-64            with self.conn.cursor() as cursor:
-65                cursor.execute(sql)
-66                rows = cursor.fetchall()
-67
-68                if not rows:
-69                    return json.dumps([{"error": f"No Files Found in Library: {library}"}])
-70
-71                result_list = [self._convert_to_json_ready(row, cursor.description) for row in rows]
-72                self.conn.commit()
-73                return json.dumps(result_list, indent=4)
-74        except Exception as e:
-75            if self.conn: self.conn.rollback()
-76            return json.dumps([{"error": f"Database Error: {str(e)}"}])
-77
-78    def getAllLibraries(self):
-79        # Hier nutzen wir nun auch die dynamische Spaltenerkennung statt der harten Liste
-80        sql = "SELECT * FROM TABLE(QSYS2.OBJECT_STATISTICS('*ALL', '*LIB')) AS X"
-81        try:
-82            with self.conn.cursor() as cursor:
-83                cursor.execute(sql)
-84                rows = cursor.fetchall()
-85
-86                if not rows:
-87                    return json.dumps([{"error": "No Libraries found"}])
-88
-89                result_list = [self._convert_to_json_ready(row, cursor.description) for row in rows]
-90                self.conn.commit()
-91                return json.dumps(result_list, indent=4)
-92        except Exception as e:
-93            print(f"Fehler bei getAllLibraries: {e}")
-94            if self.conn: self.conn.rollback()
-95            return False
-
- - - - -
- -
- - getInfoForLibrary(connection) - - - -
- -
8    def __init__(self, connection):
-9        self.conn = connection
-
- - - - -
-
-
- conn - - -
- - - - -
-
- -
- - def - getLibraryInfo(self, library: str, wantJson=True): - - - -
- -
32    def getLibraryInfo(self, library: str, wantJson=True):
-33        if not library or len(library) > 10:
-34            raise ValueError("Ungültiger Bibliotheksname (max. 10 Zeichen).")
-35
-36        sql_query = f"SELECT * FROM TABLE(QSYS2.LIBRARY_INFO(upper('{library}')))"
-37        try:
-38            with self.conn.cursor() as cursor:
-39                cursor.execute(sql_query)
-40                row = cursor.fetchone()
-41
-42                if not row:
-43                    error_msg = {"error": f"No data found for library: {library}"}
-44                    return json.dumps(error_msg, indent=4) if wantJson else ("error", error_msg["error"])
-45
-46                if wantJson:
-47                    return json.dumps(self._convert_to_json_ready(row, cursor.description), indent=4)
-48
-49                return row
-50        except Exception as e:
-51            print(f"Fehler bei getLibraryInfo: {e}")
-52            return None
-
- - - - -
-
- -
- - def - getFileInfo(self, library: str, qFiles: bool = False) -> str: - - - -
- -
54    def getFileInfo(self, library: str, qFiles: bool = False) -> str:
-55        if not library:
-56            return json.dumps([{"error": "A library name is required."}])
-57
-58        if qFiles:
-59            sql = f"SELECT * FROM QSYS2.SYSMEMBERSTAT WHERE SYSTEM_TABLE_SCHEMA = '{library.upper()}' AND SOURCE_TYPE IS NOT NULL ORDER BY SYSTEM_TABLE_MEMBER"
-60        else:
-61            sql = f"SELECT * FROM TABLE(QSYS2.OBJECT_STATISTICS('{library.upper()}', '*ALL')) AS X"
-62
-63        try:
-64            with self.conn.cursor() as cursor:
-65                cursor.execute(sql)
-66                rows = cursor.fetchall()
-67
-68                if not rows:
-69                    return json.dumps([{"error": f"No Files Found in Library: {library}"}])
-70
-71                result_list = [self._convert_to_json_ready(row, cursor.description) for row in rows]
-72                self.conn.commit()
-73                return json.dumps(result_list, indent=4)
-74        except Exception as e:
-75            if self.conn: self.conn.rollback()
-76            return json.dumps([{"error": f"Database Error: {str(e)}"}])
-
- - - - -
-
- -
- - def - getAllLibraries(self): - - - -
- -
78    def getAllLibraries(self):
-79        # Hier nutzen wir nun auch die dynamische Spaltenerkennung statt der harten Liste
-80        sql = "SELECT * FROM TABLE(QSYS2.OBJECT_STATISTICS('*ALL', '*LIB')) AS X"
-81        try:
-82            with self.conn.cursor() as cursor:
-83                cursor.execute(sql)
-84                rows = cursor.fetchall()
-85
-86                if not rows:
-87                    return json.dumps([{"error": "No Libraries found"}])
-88
-89                result_list = [self._convert_to_json_ready(row, cursor.description) for row in rows]
-90                self.conn.commit()
-91                return json.dumps(result_list, indent=4)
-92        except Exception as e:
-93            print(f"Fehler bei getAllLibraries: {e}")
-94            if self.conn: self.conn.rollback()
-95            return False
-
- - - - -
-
-
- - \ No newline at end of file diff --git a/docs/reference/iLibrary/src/getUserInfoForUser.md b/docs/reference/iLibrary/src/getUserInfoForUser.md deleted file mode 100644 index cdce757..0000000 --- a/docs/reference/iLibrary/src/getUserInfoForUser.md +++ /dev/null @@ -1,659 +0,0 @@ - - - - - - - iLibrary.src.getUserInfoForUser API documentation - - - - - - - - - -
-
-

-iLibrary.src.getUserInfoForUser

- - - - - - -
  1from os.path import join
-  2import paramiko
-  3import pyodbc
-  4import json
-  5from datetime import datetime, date
-  6from decimal import Decimal
-  7
-  8class getUserInfoForUser():
-  9    """
- 10    Handles user information retrieval and messaging functionalities.
- 11
- 12    This class provides methods to interact with the database for retrieving user information
- 13    and to send messages to specified users. It supports data retrieval in different formats
- 14    (e.g., JSON or tuple), and it enables system messaging with configurable options.
- 15
- 16    :ivar conn: Database connection object used for executing queries.
- 17    :type conn: Any
- 18    """
- 19    def getAllUsers(self, wantJson: bool = False):
- 20        """
- 21        Retrieves all user information from the database. Optionally returns the data in
- 22        JSON format depending on the provided parameter.
- 23
- 24        Retrieves a list of users stored in the database and can output the data either as
- 25        a list of tuples or in JSON format. The query fetches all fields available in the
- 26        user information database table and handles cases where no data is found.
- 27
- 28        :param wantJson: Boolean flag to indicate whether the result should be returned
- 29            in JSON format. If set to False, the result will be a list of tuples. Default
- 30            is False.
- 31        :return: The data fetched from the database. When `wantJson` is True, returns a
- 32            JSON object as a string. Otherwise, returns a list of tuples.
- 33        """
- 34        sql_query = "SELECT * FROM qsys2.user_info"
- 35
- 36        def json_serial(obj):
- 37            if hasattr(obj, 'isoformat'):
- 38                return obj.isoformat()
- 39            return str(obj)
- 40
- 41        try:
- 42            with self.conn.cursor() as cursor:
- 43                cursor.execute(sql_query)
- 44                rows = cursor.fetchall()
- 45
- 46                if not rows:
- 47                    error_msg = {'error': 'No data found'}
- 48                    return json.dumps(error_msg, indent=4) if wantJson else [("error", "No data found")]
- 49
- 50                # Get column names
- 51                columns = [column[0] for column in cursor.description]
- 52
- 53                if wantJson:
- 54                    # Create a LIST of dictionaries
- 55                    results = [dict(zip(columns, r)) for r in rows]
- 56                    return json.dumps(results, indent=4, default=json_serial)
- 57
- 58                return rows  # Returns the list of tuples
- 59
- 60        except Exception as e:
- 61            print(f"An error occurred: {e}")
- 62            return None
- 63
- 64    def getSingleUserInformation(self, username: str, wantJson: bool = False):
- 65        """
- 66        Retrieves information about a specific user from the database based on their username. The function supports
- 67        returning data either as a JSON-formatted string or as a tuple with corresponding database fields.
- 68
- 69        :param username: The username of the database user whose information is to be retrieved. Must not be empty.
- 70        :type username: str
- 71        :param wantJson: Indicates whether the output should be formatted as JSON. Defaults to False.
- 72        :type wantJson: bool
- 73        :return: A tuple containing database fields if `wantJson` is False, or a JSON-formatted string if `wantJson` is True.
- 74                 If no user is found, returns either a JSON-formatted error message or a tuple with error details, based on the
- 75                 value of `wantJson`. Returns None if an exception occurs.
- 76        :rtype: Union[tuple, str, None]
- 77        :raises ValueError: If the `username` input is empty or None.
- 78        """
- 79        if not username:
- 80          raise ValueError("A username is required.")
- 81
- 82        sql_query = f"SELECT * FROM qsys2.user_info WHERE AUTHORIZATION_NAME = upper('{username}')"
- 83
- 84        def json_serial(obj):
- 85            # Handle datetime and Decimal (common in DB2)
- 86            if hasattr(obj, 'isoformat'):
- 87                return obj.isoformat()
- 88            return str(obj)
- 89
- 90        try:
- 91            with self.conn.cursor() as cursor:
- 92                cursor.execute(sql_query)
- 93                row = cursor.fetchone()  # Since you only expect one user
- 94
- 95                if not row:
- 96                    error_msg = {'error': 'No data found for User: ' + username}
- 97                    return json.dumps(error_msg, indent=4) if wantJson else ("error", error_msg['error'])
- 98
- 99                # DYNAMICALLY get column names from the database itself
-100                columns = [column[0] for column in cursor.description]
-101                row_dict = dict(zip(columns, row))
-102
-103                if wantJson:
-104                    return json.dumps(row_dict, indent=4, default=json_serial)
-105                return row  # Returns the tuple
-106
-107        except Exception as e:
-108            print(f"An error occurred: {e}")
-109            return None
-
- - -
-
- -
- - class - getUserInfoForUser: - - - -
- -
  9class getUserInfoForUser():
- 10    """
- 11    Handles user information retrieval and messaging functionalities.
- 12
- 13    This class provides methods to interact with the database for retrieving user information
- 14    and to send messages to specified users. It supports data retrieval in different formats
- 15    (e.g., JSON or tuple), and it enables system messaging with configurable options.
- 16
- 17    :ivar conn: Database connection object used for executing queries.
- 18    :type conn: Any
- 19    """
- 20    def getAllUsers(self, wantJson: bool = False):
- 21        """
- 22        Retrieves all user information from the database. Optionally returns the data in
- 23        JSON format depending on the provided parameter.
- 24
- 25        Retrieves a list of users stored in the database and can output the data either as
- 26        a list of tuples or in JSON format. The query fetches all fields available in the
- 27        user information database table and handles cases where no data is found.
- 28
- 29        :param wantJson: Boolean flag to indicate whether the result should be returned
- 30            in JSON format. If set to False, the result will be a list of tuples. Default
- 31            is False.
- 32        :return: The data fetched from the database. When `wantJson` is True, returns a
- 33            JSON object as a string. Otherwise, returns a list of tuples.
- 34        """
- 35        sql_query = "SELECT * FROM qsys2.user_info"
- 36
- 37        def json_serial(obj):
- 38            if hasattr(obj, 'isoformat'):
- 39                return obj.isoformat()
- 40            return str(obj)
- 41
- 42        try:
- 43            with self.conn.cursor() as cursor:
- 44                cursor.execute(sql_query)
- 45                rows = cursor.fetchall()
- 46
- 47                if not rows:
- 48                    error_msg = {'error': 'No data found'}
- 49                    return json.dumps(error_msg, indent=4) if wantJson else [("error", "No data found")]
- 50
- 51                # Get column names
- 52                columns = [column[0] for column in cursor.description]
- 53
- 54                if wantJson:
- 55                    # Create a LIST of dictionaries
- 56                    results = [dict(zip(columns, r)) for r in rows]
- 57                    return json.dumps(results, indent=4, default=json_serial)
- 58
- 59                return rows  # Returns the list of tuples
- 60
- 61        except Exception as e:
- 62            print(f"An error occurred: {e}")
- 63            return None
- 64
- 65    def getSingleUserInformation(self, username: str, wantJson: bool = False):
- 66        """
- 67        Retrieves information about a specific user from the database based on their username. The function supports
- 68        returning data either as a JSON-formatted string or as a tuple with corresponding database fields.
- 69
- 70        :param username: The username of the database user whose information is to be retrieved. Must not be empty.
- 71        :type username: str
- 72        :param wantJson: Indicates whether the output should be formatted as JSON. Defaults to False.
- 73        :type wantJson: bool
- 74        :return: A tuple containing database fields if `wantJson` is False, or a JSON-formatted string if `wantJson` is True.
- 75                 If no user is found, returns either a JSON-formatted error message or a tuple with error details, based on the
- 76                 value of `wantJson`. Returns None if an exception occurs.
- 77        :rtype: Union[tuple, str, None]
- 78        :raises ValueError: If the `username` input is empty or None.
- 79        """
- 80        if not username:
- 81          raise ValueError("A username is required.")
- 82
- 83        sql_query = f"SELECT * FROM qsys2.user_info WHERE AUTHORIZATION_NAME = upper('{username}')"
- 84
- 85        def json_serial(obj):
- 86            # Handle datetime and Decimal (common in DB2)
- 87            if hasattr(obj, 'isoformat'):
- 88                return obj.isoformat()
- 89            return str(obj)
- 90
- 91        try:
- 92            with self.conn.cursor() as cursor:
- 93                cursor.execute(sql_query)
- 94                row = cursor.fetchone()  # Since you only expect one user
- 95
- 96                if not row:
- 97                    error_msg = {'error': 'No data found for User: ' + username}
- 98                    return json.dumps(error_msg, indent=4) if wantJson else ("error", error_msg['error'])
- 99
-100                # DYNAMICALLY get column names from the database itself
-101                columns = [column[0] for column in cursor.description]
-102                row_dict = dict(zip(columns, row))
-103
-104                if wantJson:
-105                    return json.dumps(row_dict, indent=4, default=json_serial)
-106                return row  # Returns the tuple
-107
-108        except Exception as e:
-109            print(f"An error occurred: {e}")
-110            return None
-
- - -

Handles user information retrieval and messaging functionalities.

- -

This class provides methods to interact with the database for retrieving user information -and to send messages to specified users. It supports data retrieval in different formats -(e.g., JSON or tuple), and it enables system messaging with configurable options.

- -

:ivar conn: Database connection object used for executing queries.

-
- - -
- -
- - def - getAllUsers(self, wantJson: bool = False): - - - -
- -
20    def getAllUsers(self, wantJson: bool = False):
-21        """
-22        Retrieves all user information from the database. Optionally returns the data in
-23        JSON format depending on the provided parameter.
-24
-25        Retrieves a list of users stored in the database and can output the data either as
-26        a list of tuples or in JSON format. The query fetches all fields available in the
-27        user information database table and handles cases where no data is found.
-28
-29        :param wantJson: Boolean flag to indicate whether the result should be returned
-30            in JSON format. If set to False, the result will be a list of tuples. Default
-31            is False.
-32        :return: The data fetched from the database. When `wantJson` is True, returns a
-33            JSON object as a string. Otherwise, returns a list of tuples.
-34        """
-35        sql_query = "SELECT * FROM qsys2.user_info"
-36
-37        def json_serial(obj):
-38            if hasattr(obj, 'isoformat'):
-39                return obj.isoformat()
-40            return str(obj)
-41
-42        try:
-43            with self.conn.cursor() as cursor:
-44                cursor.execute(sql_query)
-45                rows = cursor.fetchall()
-46
-47                if not rows:
-48                    error_msg = {'error': 'No data found'}
-49                    return json.dumps(error_msg, indent=4) if wantJson else [("error", "No data found")]
-50
-51                # Get column names
-52                columns = [column[0] for column in cursor.description]
-53
-54                if wantJson:
-55                    # Create a LIST of dictionaries
-56                    results = [dict(zip(columns, r)) for r in rows]
-57                    return json.dumps(results, indent=4, default=json_serial)
-58
-59                return rows  # Returns the list of tuples
-60
-61        except Exception as e:
-62            print(f"An error occurred: {e}")
-63            return None
-
- - -

Retrieves all user information from the database. Optionally returns the data in -JSON format depending on the provided parameter.

- -

Retrieves a list of users stored in the database and can output the data either as -a list of tuples or in JSON format. The query fetches all fields available in the -user information database table and handles cases where no data is found.

- -
Parameters
- -
    -
  • wantJson: Boolean flag to indicate whether the result should be returned -in JSON format. If set to False, the result will be a list of tuples. Default -is False.
  • -
- -
Returns
- -
-

The data fetched from the database. When wantJson is True, returns a - JSON object as a string. Otherwise, returns a list of tuples.

-
-
- - -
-
- -
- - def - getSingleUserInformation(self, username: str, wantJson: bool = False): - - - -
- -
 65    def getSingleUserInformation(self, username: str, wantJson: bool = False):
- 66        """
- 67        Retrieves information about a specific user from the database based on their username. The function supports
- 68        returning data either as a JSON-formatted string or as a tuple with corresponding database fields.
- 69
- 70        :param username: The username of the database user whose information is to be retrieved. Must not be empty.
- 71        :type username: str
- 72        :param wantJson: Indicates whether the output should be formatted as JSON. Defaults to False.
- 73        :type wantJson: bool
- 74        :return: A tuple containing database fields if `wantJson` is False, or a JSON-formatted string if `wantJson` is True.
- 75                 If no user is found, returns either a JSON-formatted error message or a tuple with error details, based on the
- 76                 value of `wantJson`. Returns None if an exception occurs.
- 77        :rtype: Union[tuple, str, None]
- 78        :raises ValueError: If the `username` input is empty or None.
- 79        """
- 80        if not username:
- 81          raise ValueError("A username is required.")
- 82
- 83        sql_query = f"SELECT * FROM qsys2.user_info WHERE AUTHORIZATION_NAME = upper('{username}')"
- 84
- 85        def json_serial(obj):
- 86            # Handle datetime and Decimal (common in DB2)
- 87            if hasattr(obj, 'isoformat'):
- 88                return obj.isoformat()
- 89            return str(obj)
- 90
- 91        try:
- 92            with self.conn.cursor() as cursor:
- 93                cursor.execute(sql_query)
- 94                row = cursor.fetchone()  # Since you only expect one user
- 95
- 96                if not row:
- 97                    error_msg = {'error': 'No data found for User: ' + username}
- 98                    return json.dumps(error_msg, indent=4) if wantJson else ("error", error_msg['error'])
- 99
-100                # DYNAMICALLY get column names from the database itself
-101                columns = [column[0] for column in cursor.description]
-102                row_dict = dict(zip(columns, row))
-103
-104                if wantJson:
-105                    return json.dumps(row_dict, indent=4, default=json_serial)
-106                return row  # Returns the tuple
-107
-108        except Exception as e:
-109            print(f"An error occurred: {e}")
-110            return None
-
- - -

Retrieves information about a specific user from the database based on their username. The function supports -returning data either as a JSON-formatted string or as a tuple with corresponding database fields.

- -
Parameters
- -
    -
  • username: The username of the database user whose information is to be retrieved. Must not be empty.
  • -
  • wantJson: Indicates whether the output should be formatted as JSON. Defaults to False.
  • -
- -
Returns
- -
-

A tuple containing database fields if wantJson is False, or a JSON-formatted string if wantJson is True. - If no user is found, returns either a JSON-formatted error message or a tuple with error details, based on the - value of wantJson. Returns None if an exception occurs.

-
- -
Raises
- -
    -
  • ValueError: If the username input is empty or None.
  • -
-
- - -
-
-
- - \ No newline at end of file diff --git a/docs/reference/iLibrary/src/saveLibrary.md b/docs/reference/iLibrary/src/saveLibrary.md deleted file mode 100644 index 5cb37ab..0000000 --- a/docs/reference/iLibrary/src/saveLibrary.md +++ /dev/null @@ -1,1466 +0,0 @@ - - - - - - - iLibrary.src.saveLibrary API documentation - - - - - - - - - -
-
-

-iLibrary.src.saveLibrary

- - - - - - -
  1from _ast import Raise
-  2from os.path import join
-  3import paramiko
-  4import pyodbc
-  5import json
-  6from datetime import datetime, date
-  7from decimal import Decimal
-  8from typing import Union
-  9from pathlib import PureWindowsPath
- 10
- 11
- 12class saveLibrary:
- 13
- 14    def saveLibrary(self,
- 15                    library: str,
- 16                    saveFileName: str,
- 17                    dev: str = None,
- 18                    vol: str = None,
- 19                    toLibrary: str = None,
- 20                    description: str = None,
- 21                    localPath: str = None,
- 22                    remPath: str = None,
- 23                    getZip: bool = False,
- 24                    port: int = None,
- 25                    remSavf=True,
- 26                    version: str = None,
- 27                    max_records: Union[int, str, None] = None,
- 28                    asp: Union[int, str, None] = None,
- 29                    waitFile: Union[int, str, None] = None,
- 30                    share: str = None,
- 31                    authority: str = None
- 32                    ) -> bool:
- 33        """
- 34        Saves a library to a specified save file, providing options for further customization such
- 35        as setting the target release, saving as a zip file, specifying the device, volume, and more.
- 36
- 37        :param library: The name of the library to be saved. Must be a valid library name or one of
- 38            the predefined options such as '*NONSYS', '*ALLUSR', '*IBM', etc.
- 39        :type library: str
- 40        :param saveFileName: The name of the save file where the library will be saved.
- 41        :type saveFileName: str
- 42        :param dev: The target device for the save operation. Defaults to '*SAVF' if not provided.
- 43        :type dev: str, optional
- 44        :param vol: Specifies the volume to be used. Use ‘*MOUNTED’ to refer to the mounted volume.
- 45        :type vol: str, optional
- 46        :param toLibrary: Target library where the save file will be temporarily stored. Defaults
- 47            to the value of `library` if not specified.
- 48        :type toLibrary: str, optional
- 49        :param description: An optional description for the save file to be created.
- 50        :type description: str, optional
- 51        :param localPath: The local path where the save file will be downloaded if `getZip` is set
- 52            to True. Must be an absolute path.
- 53        :type localPath: str, optional
- 54        :param remPath: The remote directory path on the target system to temporarily store the
- 55            save file if `getZip` is set to True. Must be an absolute path.
- 56        :type remPath: str, optional
- 57        :param getZip: A flag that determines whether the save file should be archived into a zip
- 58            file and downloaded locally.
- 59        :type getZip: bool
- 60        :param port: Specifies the port to be used for transferring the save file when `getZip` is
- 61            enabled.
- 62        :type port: int, optional
- 63        :param remSavf: A flag indicating whether the save file should be removed from the remote
- 64            target system after a successful save.
- 65        :type remSavf: bool
- 66        :param version: The target release version for the save operation. Valid values include
- 67            ‘*CURRENT’, or specific OS versions like 'V1R1M0', 'V2R3M0', and so on.
- 68        :type version: str, optional
- 69        :param max_records: Optional parameter for specifying the maximum number of records in
- 70            the save file.
- 71        :type max_records: Union[int, str, None], optional
- 72        :param asp: Auxiliary storage pool (ASP) device number or name if applicable.
- 73        :type asp: Union[int, str, None], optional
- 74        :param waitFile: The amount of time to wait for file access locks to be released.
- 75        :type waitFile: Union[int, str, None], optional
- 76        :param share: Specifies the share handling for threads or users accessing the save file.
- 77        :type share: str, optional
- 78        :param authority: Authority option to set for the save file being saved.
- 79        :type authority: str, optional
- 80        :return: A boolean indicating whether the library was successfully saved. Returns True on
- 81            success or False on failure.
- 82        :rtype: bool
- 83        """
- 84        # Target Release List
- 85        trgList: list = ["V1R1M0", "V1R1M2", "V1R2M0", "V1R3M0", "V2R1M0", "V2R1M1",
- 86                         "V2R2M0", "V2R3M0", "V3R0M5", "V3R1M0", "V3R2M0", "V3R6M0",
- 87                         "V3R7M0", "V4R1M0", "V4R2M0", "V4R3M0", "V4R4M0", "V4R5M0",
- 88                         "V5R1M0", "V5R2M0", "V5R3M0", "V5R4M0", "V6R1M0", "V6R1M1",
- 89                         "V7R1M0", "V7R2M0", "V7R3M0", "V7R4M0", "V7R5M0", "V7R6M0"]
- 90
- 91        # check if something missing from the Arguments
- 92        # check if Library is empty or not
- 93        if not library:
- 94            raise ValueError("A library name is required.")
- 95        # check if saveFileName is empty or not
- 96        if not saveFileName:
- 97            raise ValueError("A save file name is required.")
- 98        # check if toLibrary is empty or not
- 99        if not toLibrary:
-100            toLibrary = library
-101        # check if user want the SaveFile as ZIP File
-102        if getZip:
-103            if not remPath:
-104                raise ValueError("A remote path is required. Use 'remPath' instead.")
-105            elif remPath[-1] == '/':
-106                remPath = remPath[:-1]
-107            if not localPath:
-108                raise ValueError("A local path is required. Use 'localPath' instead.")
-109            elif localPath[-1] == '/':
-110                localPath = localPath[:-1]
-111        # check wich Version of SaveFile is wanted
-112        if not version in list(trgList):
-113            version = "*CURRENT"
-114        else:
-115            version = version.upper()
-116        command_str: str = f'SAVLIB'
-117
-118        # check if Library is valid or not
-119        validated_library = self.__validate_max_value(value=library, param_name='library',
-120                                                      str_format=['*NONSYS', '*ALLUSR', '*IBM', '*SELECT', '*USRSPC',
-121                                                                  library])
-122        if validated_library:
-123            command_str += f' LIB({validated_library})'
-124        else:
-125            library_str = str(library)
-126            raise ValueError(
-127                f"The library '{library_str}' is not valid. Must be one of the specified strings or a valid number.")
-128        # check Dev - Device
-129        if not dev in ['*SAVF', '*MEDDFN']:
-130            command_str += f' DEV(*SAVF)'
-131        else:
-132            command_str += f' DEV({dev.upper()})'
-133        if vol is not None and vol == '*MOUNTED':
-134            command_str += f' VOL({vol})'
-135        # starting with mem main Sourcecode of saveLLibrary
-136        if self.__crtsavf(saveFileName, toLibrary, description, max_records=max_records, asp=asp, waitFile=waitFile,
-137                          share=share, authority=authority):
-138            # command_str: str = f"SAVLIB LIB({library.strip()}) DEV(*SAVF) SAVF({toLibrary.strip()}/{saveFileName.strip()}) TGTRLS({version.strip()})"
-139            command_str += f" SAVF({toLibrary.strip()}/{saveFileName.strip()}) TGTRLS({version.strip()})"
-140            #print(command_str)
-141            try:
-142                with self.conn.cursor() as cursor:
-143                    # execute the Command for creating a Savefile.
-144                    cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str))
-145                    if getZip:
-146                        try:
-147                            remote_temp_savf_path = join(remPath, saveFileName.upper() + '.savf')
-148
-149                            destination_local_path = join(localPath, saveFileName.upper() + '.savf')
-150                            command_str = (
-151                                f"CPYTOSTMF FROMMBR('/QSYS.LIB/{toLibrary.upper().strip()}.LIB/{saveFileName.upper().strip()}.FILE') "
-152                                f"TOSTMF('{remote_temp_savf_path.strip()}') STMFOPT(*REPLACE)"
-153                            )
-154
-155                            # Execute the command on the remote system
-156                            cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str,))
-157
-158                            if self.__getSavFile(localFilePath=destination_local_path,
-159                                                 remotePath=remote_temp_savf_path, port=port):
-160                                rmvCommand = f"QSH CMD('rm -r {remote_temp_savf_path}')"
-161                                cursor.execute("CALL QSYS2.QCMDEXC(?)", (rmvCommand))
-162                            else:
-163                                raise ValueError("Something went wrong. With downloading the Save File.")
-164                            if remSavf:
-165                                if not self.removeFile(library=toLibrary, saveFileName=saveFileName):
-166                                    raise ValueError(f"The Save File {saveFileName} was not successfully removed.")
-167
-168                        except Exception as e:
-169                            self.__handle_error(error=e, pgm="saveLibrary - Transfer")
-170
-171            except Exception as e:
-172                self.__handle_error(error=e, pgm="saveLibrary")
-173                self.conn.rollback()
-174                return False
-175            else:
-176                self.conn.commit()
-177                if getZip:
-178                    print(f"File successfully downloaded to: {destination_local_path}")
-179                    return True
-180
-181                print(f"Successfully saved in the Library '{library}' successfully.")
-182                return True
-183
-184        return False
-185
-186    # ------------------------------------------------------
-187    # sub Function: create the Savefile on the AS400
-188    # ------------------------------------------------------
-189    def __crtsavf(self,
-190                  saveFileName: str,
-191                  library: str,
-192                  description: str = None,
-193                  max_records: Union[int, str, None] = None,
-194                  asp: Union[int, str, None] = None,
-195                  waitFile: Union[int, str, None] = None,
-196                  share: str = None,
-197                  authority: str = None
-198                  ) -> bool:
-199        """
-200            Sub-function to create a save file on the IBM i server.
-201
-202            This function executes the `CRTSAVF` (Create Save File) CL command
-203            to create a new save file in the specified library. This is a
-204            prerequisite for saving a library's contents.
-205
-206            Args:
-207                saveFileName (str): The name of the save file to be created.
-208                                    This will be the AS/400 object name.
-209                library (str): The name of the library where the save file will be created.
-210                description (str, optional): A text description for the save file. Defaults to None.
-211
-212            Returns:
-213                bool: True if the save file was created successfully, False otherwise.
-214        """
-215        # check is a parameter empty or not
-216
-217        if not saveFileName:
-218            raise ValueError("A file name is required.")
-219        if not library:
-220            raise ValueError("A library name is required.")
-221        if not description:
-222            description = 'A SaveFile from iLibrary'
-223
-224        command_str: str = f"CRTSAVF FILE({library.upper().strip()}/{saveFileName.upper().strip()}) TEXT('{description.strip()}')"
-225
-226        # check max_records for MAXRCDS parameter
-227        if self.__validate_max_value(value=max_records, param_name='max_records', str_format=['*NOMAX'],
-228                                     max_limit=4293525600) and not None:
-229            command_str += f" MAXRCDS({max_records})"
-230        # check asp for ASP 2147483647
-231        if self.__validate_max_value(value=asp, param_name='asp', str_format=['*LIBASP'], max_limit=32) and not None:
-232            command_str += f" ASP({asp})"
-233        if self.__validate_max_value(value=waitFile, param_name='waitFile', str_format=['*IMMED', '*CLS'],
-234                                     max_limit=2147483647) and not None:
-235            command_str += f" WAITFILE({waitFile})"
-236        if self.__validate_max_value(value=share, param_name='share', str_format=['*YES', '*NO']) and not None:
-237            command_str += f" SHARE({share})"
-238
-239        if authority is not None:
-240            upper_authority = authority.upper()
-241
-242            # 1. Check for custom authority (not in list AND up to 10 chars)
-243            if upper_authority not in ['*EXCLUDE', '*ALL', '*CHANGE', '*LIBCRTAUT', '*USE'] and len(
-244                    upper_authority) <= 10:
-245                # **CORRECTION 1: Use upper_authority here, not the undefined 'auth'**
-246                command_str += f" AUT({upper_authority})"
-247                # The 'pass' statements are redundant and can be removed
-248
-249            # 2. Add an 'elif' to handle the case where it IS one of the standard values
-250            elif upper_authority in ['*EXCLUDE', '*ALL', '*CHANGE', '*LIBCRTAUT', '*USE']:
-251                command_str += f" AUT({upper_authority})"
-252
-253
-254        try:
-255            with self.conn.cursor() as cursor:
-256                # execute the Command for creating a Savefile.
-257                cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str))
-258
-259        except Exception as e:
-260            self.__handle_error(error=e, pgm="__crtsavf")
-261            # remove a SAVF if its exists and we got an error
-262            if e.args[0] == 'HY000':
-263                sql = """
-264                      SELECT 1
-265                      FROM QSYS2.SAVE_FILE_INFO
-266                      WHERE SAVE_FILE_LIBRARY = ? \
-267                        AND SAVE_FILE = ?
-268                          FETCH FIRST 1 ROW ONLY \
-269                      """
-270                cursor = self.conn.cursor()
-271                cursor.execute(sql, library, saveFileName)
-272                result = cursor.fetchone()
-273                if result is not None:
-274                    self.removeFile(library=library, saveFileName=saveFileName)
-275            self.conn.rollback()
-276            raise ValueError(e)
-277        else:
-278            self.conn.commit()
-279            return True
-280
-281    # --------------------------------------------------------------------------
-282    # __validate_max_value - Helper Function for checking parameter
-283    # --------------------------------------------------------------------------
-284    def __validate_max_value(self,
-285                             value: Union[int, str, None],
-286                             param_name: str,
-287                             str_format: list[str],
-288                             min_limit: int = 1,
-289                             max_limit: int = None
-290                             ) -> Union[int, str, bool]:  # Includes bool as requested
-291        """
-292        Validates an input value for 'MAX' type parameters against a custom range.
-293        Handles special strings defined in str_format and numeric values.
-294
-295        Returns: The validated integer, the standardized special string, or False on failure (if no exception is raised).
-296        Raises: ValueError for invalid string format or out-of-range number.
-297        """
-298
-299        # Helper for clear error messages
-300        str_options = ", ".join([f"'{s}'" for s in str_format])
-301
-302        # 1. Handle special string
-303        if isinstance(value, str):
-304            upper_value = value.upper()
-305
-306            for special_value in str_format:
-307                normalized_special_value = special_value.upper()
-308
-309                if upper_value == special_value.upper() or upper_value == normalized_special_value:
-310                    # Found a match! Return the official, fully formatted string.
-311                    return special_value
-312
-313        # 2. Attempt Numeric Conversion (handles int and string-of-int)
-314        if value is not None:
-315            try:
-316                numeric_value = int(value)
-317            except ValueError:
-318                # Value is an invalid string (e.g., 'hello')
-319                raise ValueError(
-320                    f"Invalid value for {param_name}. Must be '{str_format}' or a number "
-321                    f"between {min_limit} and {max_limit:,}."
-322                )
-323        else:
-324            # If the value is None
-325            return False
-326
-327        # 3. Check Numeric Range
-328        if min_limit <= numeric_value <= max_limit:
-329            return numeric_value
-330        else:
-331            # Number is out of range
-332            raise ValueError(
-333                f"Invalid numeric value for {param_name}. Must be between {min_limit} and {max_limit:,}. "
-334                f"Received: {numeric_value}"
-335            )
-336
-337    # ------------------------------------------------------
-338    # getZipFile - getting the Zipfile from the SaveFile
-339    # ------------------------------------------------------
-340    def __getSavFile(self,
-341                     localFilePath: str,
-342                     remotePath: str,
-343                     port: int = None
-344                     ) -> bool:
-345        """
-346            Downloads a file from the remote IBM i via SFTP.
-347
-348            This method uses Paramiko to establish a secure shell (SSH) connection and
-349            then an SFTP session to transfer a file from a specified remote location
-350            on the IBM i's IFS to a local path.
-351
-352            Args:
-353                localFilePath (str): The full path to the file on the remote IBM i's IFS.
-354                remotePath (str): The full path on the local machine where the file
-355                                       will be saved. For example, '/Users/user/Documents/somefile.savf'.
-356                port (int, optional): The port to connect to the IBMi server. Defaults to None.
-357
-358            Returns:
-359                bool: True if the file was downloaded successfully, False otherwise.
-360
-361            Raises:
-362                ValueError: If either the remote_file_path or local_save_path is not provided.
-363        """
-364        if not localFilePath:
-365            print("Error: A local file path is required.")
-366            return False
-367        if not remotePath:
-368            print("Error: A remote path is required.")
-369            return False
-370        if not port:
-371            port = 2222
-372
-373        remotePath = PureWindowsPath(remotePath).as_posix()
-374        ssh_client = paramiko.SSHClient()
-375
-376        ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-377
-378        try:
-379            with ssh_client:
-380                ssh_client.connect(
-381                    hostname=self.db_host,
-382                    username=self.db_user,
-383                    password=self.db_password,
-384                    port=port
-385                )
-386                with ssh_client.open_sftp() as ftp_client:
-387                    ftp_client.get(remotePath, localFilePath)
-388                    return True
-389
-390        except paramiko.ssh_exception.AuthenticationException as e:
-391            print(f"Authentication failed. Check your username and password: {e}")
-392            return False
-393        except paramiko.ssh_exception.SSHException as e:
-394            print(f"SSH error occurred: {e}")
-395            return False
-396        except FileNotFoundError as e:
-397            print(f"File not found on the remote host: {e}")
-398            return False
-399
-400        finally:
-401            pass
-402
-403    def removeFile(self, library: str, saveFileName: str) -> bool:
-404        """
-405        Removes a save file from the specified library.
-406
-407        This function executes the system command to delete a save file from an IBM i
-408        system. It connects to the database through a cursor, and attempts to perform
-409        the operation. If an error is encountered during execution, the function
-410        rolls back the transaction and logs the error. On success, the transaction
-411        is committed.
-412
-413        :param library: The name of the library containing the save file to be removed.
-414        :type library: str
-415        :param saveFileName: The name of the save file to be removed.
-416        :type saveFileName: str
-417        :return: True if the save file is removed successfully, otherwise False.
-418        :rtype: bool
-419        """
-420        command_str: str = f"DLTF FILE({library.upper()}/{saveFileName.upper()})"
-421        try:
-422            with self.conn.cursor() as cursor:
-423                # execute the Command for deleting a Savefile.
-424                cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str))
-425
-426        except Exception as e:
-427            self.__handle_error(error=e, pgm="removeFile")
-428            self.conn.rollback()
-429            return False
-430        else:
-431            self.conn.commit()
-432            return True
-433
-434    def __handle_error(self, error, pgm: str):
-435        """
-436        Handles errors encountered during the execution of a command.
-437
-438        This method processes an error raised during the execution of a command in a
-439        specific function and extracts detailed error information including SQLSTATE
-440        and the error message. The formatted details are printed to the console for
-441        debugging purposes.
-442
-443        :param error: The error object encountered during command execution.
-444        :type error: Exception
-445        :param pgm: The name of the function where the error occurred.
-446        :type pgm: str
-447        :return: None
-448        """
-449        print("-------------------------------------------------------------")
-450        print(f"An error occurred while executing command in function {pgm}:")
-451        sqlstate = error.args[0]
-452        error_message = error.args[1]
-453
-454        print(f"SQLSTATE: {sqlstate}")
-455        print(f"Message: {error_message}")
-
- - -
-
- -
- - class - saveLibrary: - - - -
- -
 13class saveLibrary:
- 14
- 15    def saveLibrary(self,
- 16                    library: str,
- 17                    saveFileName: str,
- 18                    dev: str = None,
- 19                    vol: str = None,
- 20                    toLibrary: str = None,
- 21                    description: str = None,
- 22                    localPath: str = None,
- 23                    remPath: str = None,
- 24                    getZip: bool = False,
- 25                    port: int = None,
- 26                    remSavf=True,
- 27                    version: str = None,
- 28                    max_records: Union[int, str, None] = None,
- 29                    asp: Union[int, str, None] = None,
- 30                    waitFile: Union[int, str, None] = None,
- 31                    share: str = None,
- 32                    authority: str = None
- 33                    ) -> bool:
- 34        """
- 35        Saves a library to a specified save file, providing options for further customization such
- 36        as setting the target release, saving as a zip file, specifying the device, volume, and more.
- 37
- 38        :param library: The name of the library to be saved. Must be a valid library name or one of
- 39            the predefined options such as '*NONSYS', '*ALLUSR', '*IBM', etc.
- 40        :type library: str
- 41        :param saveFileName: The name of the save file where the library will be saved.
- 42        :type saveFileName: str
- 43        :param dev: The target device for the save operation. Defaults to '*SAVF' if not provided.
- 44        :type dev: str, optional
- 45        :param vol: Specifies the volume to be used. Use ‘*MOUNTED’ to refer to the mounted volume.
- 46        :type vol: str, optional
- 47        :param toLibrary: Target library where the save file will be temporarily stored. Defaults
- 48            to the value of `library` if not specified.
- 49        :type toLibrary: str, optional
- 50        :param description: An optional description for the save file to be created.
- 51        :type description: str, optional
- 52        :param localPath: The local path where the save file will be downloaded if `getZip` is set
- 53            to True. Must be an absolute path.
- 54        :type localPath: str, optional
- 55        :param remPath: The remote directory path on the target system to temporarily store the
- 56            save file if `getZip` is set to True. Must be an absolute path.
- 57        :type remPath: str, optional
- 58        :param getZip: A flag that determines whether the save file should be archived into a zip
- 59            file and downloaded locally.
- 60        :type getZip: bool
- 61        :param port: Specifies the port to be used for transferring the save file when `getZip` is
- 62            enabled.
- 63        :type port: int, optional
- 64        :param remSavf: A flag indicating whether the save file should be removed from the remote
- 65            target system after a successful save.
- 66        :type remSavf: bool
- 67        :param version: The target release version for the save operation. Valid values include
- 68            ‘*CURRENT’, or specific OS versions like 'V1R1M0', 'V2R3M0', and so on.
- 69        :type version: str, optional
- 70        :param max_records: Optional parameter for specifying the maximum number of records in
- 71            the save file.
- 72        :type max_records: Union[int, str, None], optional
- 73        :param asp: Auxiliary storage pool (ASP) device number or name if applicable.
- 74        :type asp: Union[int, str, None], optional
- 75        :param waitFile: The amount of time to wait for file access locks to be released.
- 76        :type waitFile: Union[int, str, None], optional
- 77        :param share: Specifies the share handling for threads or users accessing the save file.
- 78        :type share: str, optional
- 79        :param authority: Authority option to set for the save file being saved.
- 80        :type authority: str, optional
- 81        :return: A boolean indicating whether the library was successfully saved. Returns True on
- 82            success or False on failure.
- 83        :rtype: bool
- 84        """
- 85        # Target Release List
- 86        trgList: list = ["V1R1M0", "V1R1M2", "V1R2M0", "V1R3M0", "V2R1M0", "V2R1M1",
- 87                         "V2R2M0", "V2R3M0", "V3R0M5", "V3R1M0", "V3R2M0", "V3R6M0",
- 88                         "V3R7M0", "V4R1M0", "V4R2M0", "V4R3M0", "V4R4M0", "V4R5M0",
- 89                         "V5R1M0", "V5R2M0", "V5R3M0", "V5R4M0", "V6R1M0", "V6R1M1",
- 90                         "V7R1M0", "V7R2M0", "V7R3M0", "V7R4M0", "V7R5M0", "V7R6M0"]
- 91
- 92        # check if something missing from the Arguments
- 93        # check if Library is empty or not
- 94        if not library:
- 95            raise ValueError("A library name is required.")
- 96        # check if saveFileName is empty or not
- 97        if not saveFileName:
- 98            raise ValueError("A save file name is required.")
- 99        # check if toLibrary is empty or not
-100        if not toLibrary:
-101            toLibrary = library
-102        # check if user want the SaveFile as ZIP File
-103        if getZip:
-104            if not remPath:
-105                raise ValueError("A remote path is required. Use 'remPath' instead.")
-106            elif remPath[-1] == '/':
-107                remPath = remPath[:-1]
-108            if not localPath:
-109                raise ValueError("A local path is required. Use 'localPath' instead.")
-110            elif localPath[-1] == '/':
-111                localPath = localPath[:-1]
-112        # check wich Version of SaveFile is wanted
-113        if not version in list(trgList):
-114            version = "*CURRENT"
-115        else:
-116            version = version.upper()
-117        command_str: str = f'SAVLIB'
-118
-119        # check if Library is valid or not
-120        validated_library = self.__validate_max_value(value=library, param_name='library',
-121                                                      str_format=['*NONSYS', '*ALLUSR', '*IBM', '*SELECT', '*USRSPC',
-122                                                                  library])
-123        if validated_library:
-124            command_str += f' LIB({validated_library})'
-125        else:
-126            library_str = str(library)
-127            raise ValueError(
-128                f"The library '{library_str}' is not valid. Must be one of the specified strings or a valid number.")
-129        # check Dev - Device
-130        if not dev in ['*SAVF', '*MEDDFN']:
-131            command_str += f' DEV(*SAVF)'
-132        else:
-133            command_str += f' DEV({dev.upper()})'
-134        if vol is not None and vol == '*MOUNTED':
-135            command_str += f' VOL({vol})'
-136        # starting with mem main Sourcecode of saveLLibrary
-137        if self.__crtsavf(saveFileName, toLibrary, description, max_records=max_records, asp=asp, waitFile=waitFile,
-138                          share=share, authority=authority):
-139            # command_str: str = f"SAVLIB LIB({library.strip()}) DEV(*SAVF) SAVF({toLibrary.strip()}/{saveFileName.strip()}) TGTRLS({version.strip()})"
-140            command_str += f" SAVF({toLibrary.strip()}/{saveFileName.strip()}) TGTRLS({version.strip()})"
-141            #print(command_str)
-142            try:
-143                with self.conn.cursor() as cursor:
-144                    # execute the Command for creating a Savefile.
-145                    cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str))
-146                    if getZip:
-147                        try:
-148                            remote_temp_savf_path = join(remPath, saveFileName.upper() + '.savf')
-149
-150                            destination_local_path = join(localPath, saveFileName.upper() + '.savf')
-151                            command_str = (
-152                                f"CPYTOSTMF FROMMBR('/QSYS.LIB/{toLibrary.upper().strip()}.LIB/{saveFileName.upper().strip()}.FILE') "
-153                                f"TOSTMF('{remote_temp_savf_path.strip()}') STMFOPT(*REPLACE)"
-154                            )
-155
-156                            # Execute the command on the remote system
-157                            cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str,))
-158
-159                            if self.__getSavFile(localFilePath=destination_local_path,
-160                                                 remotePath=remote_temp_savf_path, port=port):
-161                                rmvCommand = f"QSH CMD('rm -r {remote_temp_savf_path}')"
-162                                cursor.execute("CALL QSYS2.QCMDEXC(?)", (rmvCommand))
-163                            else:
-164                                raise ValueError("Something went wrong. With downloading the Save File.")
-165                            if remSavf:
-166                                if not self.removeFile(library=toLibrary, saveFileName=saveFileName):
-167                                    raise ValueError(f"The Save File {saveFileName} was not successfully removed.")
-168
-169                        except Exception as e:
-170                            self.__handle_error(error=e, pgm="saveLibrary - Transfer")
-171
-172            except Exception as e:
-173                self.__handle_error(error=e, pgm="saveLibrary")
-174                self.conn.rollback()
-175                return False
-176            else:
-177                self.conn.commit()
-178                if getZip:
-179                    print(f"File successfully downloaded to: {destination_local_path}")
-180                    return True
-181
-182                print(f"Successfully saved in the Library '{library}' successfully.")
-183                return True
-184
-185        return False
-186
-187    # ------------------------------------------------------
-188    # sub Function: create the Savefile on the AS400
-189    # ------------------------------------------------------
-190    def __crtsavf(self,
-191                  saveFileName: str,
-192                  library: str,
-193                  description: str = None,
-194                  max_records: Union[int, str, None] = None,
-195                  asp: Union[int, str, None] = None,
-196                  waitFile: Union[int, str, None] = None,
-197                  share: str = None,
-198                  authority: str = None
-199                  ) -> bool:
-200        """
-201            Sub-function to create a save file on the IBM i server.
-202
-203            This function executes the `CRTSAVF` (Create Save File) CL command
-204            to create a new save file in the specified library. This is a
-205            prerequisite for saving a library's contents.
-206
-207            Args:
-208                saveFileName (str): The name of the save file to be created.
-209                                    This will be the AS/400 object name.
-210                library (str): The name of the library where the save file will be created.
-211                description (str, optional): A text description for the save file. Defaults to None.
-212
-213            Returns:
-214                bool: True if the save file was created successfully, False otherwise.
-215        """
-216        # check is a parameter empty or not
-217
-218        if not saveFileName:
-219            raise ValueError("A file name is required.")
-220        if not library:
-221            raise ValueError("A library name is required.")
-222        if not description:
-223            description = 'A SaveFile from iLibrary'
-224
-225        command_str: str = f"CRTSAVF FILE({library.upper().strip()}/{saveFileName.upper().strip()}) TEXT('{description.strip()}')"
-226
-227        # check max_records for MAXRCDS parameter
-228        if self.__validate_max_value(value=max_records, param_name='max_records', str_format=['*NOMAX'],
-229                                     max_limit=4293525600) and not None:
-230            command_str += f" MAXRCDS({max_records})"
-231        # check asp for ASP 2147483647
-232        if self.__validate_max_value(value=asp, param_name='asp', str_format=['*LIBASP'], max_limit=32) and not None:
-233            command_str += f" ASP({asp})"
-234        if self.__validate_max_value(value=waitFile, param_name='waitFile', str_format=['*IMMED', '*CLS'],
-235                                     max_limit=2147483647) and not None:
-236            command_str += f" WAITFILE({waitFile})"
-237        if self.__validate_max_value(value=share, param_name='share', str_format=['*YES', '*NO']) and not None:
-238            command_str += f" SHARE({share})"
-239
-240        if authority is not None:
-241            upper_authority = authority.upper()
-242
-243            # 1. Check for custom authority (not in list AND up to 10 chars)
-244            if upper_authority not in ['*EXCLUDE', '*ALL', '*CHANGE', '*LIBCRTAUT', '*USE'] and len(
-245                    upper_authority) <= 10:
-246                # **CORRECTION 1: Use upper_authority here, not the undefined 'auth'**
-247                command_str += f" AUT({upper_authority})"
-248                # The 'pass' statements are redundant and can be removed
-249
-250            # 2. Add an 'elif' to handle the case where it IS one of the standard values
-251            elif upper_authority in ['*EXCLUDE', '*ALL', '*CHANGE', '*LIBCRTAUT', '*USE']:
-252                command_str += f" AUT({upper_authority})"
-253
-254
-255        try:
-256            with self.conn.cursor() as cursor:
-257                # execute the Command for creating a Savefile.
-258                cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str))
-259
-260        except Exception as e:
-261            self.__handle_error(error=e, pgm="__crtsavf")
-262            # remove a SAVF if its exists and we got an error
-263            if e.args[0] == 'HY000':
-264                sql = """
-265                      SELECT 1
-266                      FROM QSYS2.SAVE_FILE_INFO
-267                      WHERE SAVE_FILE_LIBRARY = ? \
-268                        AND SAVE_FILE = ?
-269                          FETCH FIRST 1 ROW ONLY \
-270                      """
-271                cursor = self.conn.cursor()
-272                cursor.execute(sql, library, saveFileName)
-273                result = cursor.fetchone()
-274                if result is not None:
-275                    self.removeFile(library=library, saveFileName=saveFileName)
-276            self.conn.rollback()
-277            raise ValueError(e)
-278        else:
-279            self.conn.commit()
-280            return True
-281
-282    # --------------------------------------------------------------------------
-283    # __validate_max_value - Helper Function for checking parameter
-284    # --------------------------------------------------------------------------
-285    def __validate_max_value(self,
-286                             value: Union[int, str, None],
-287                             param_name: str,
-288                             str_format: list[str],
-289                             min_limit: int = 1,
-290                             max_limit: int = None
-291                             ) -> Union[int, str, bool]:  # Includes bool as requested
-292        """
-293        Validates an input value for 'MAX' type parameters against a custom range.
-294        Handles special strings defined in str_format and numeric values.
-295
-296        Returns: The validated integer, the standardized special string, or False on failure (if no exception is raised).
-297        Raises: ValueError for invalid string format or out-of-range number.
-298        """
-299
-300        # Helper for clear error messages
-301        str_options = ", ".join([f"'{s}'" for s in str_format])
-302
-303        # 1. Handle special string
-304        if isinstance(value, str):
-305            upper_value = value.upper()
-306
-307            for special_value in str_format:
-308                normalized_special_value = special_value.upper()
-309
-310                if upper_value == special_value.upper() or upper_value == normalized_special_value:
-311                    # Found a match! Return the official, fully formatted string.
-312                    return special_value
-313
-314        # 2. Attempt Numeric Conversion (handles int and string-of-int)
-315        if value is not None:
-316            try:
-317                numeric_value = int(value)
-318            except ValueError:
-319                # Value is an invalid string (e.g., 'hello')
-320                raise ValueError(
-321                    f"Invalid value for {param_name}. Must be '{str_format}' or a number "
-322                    f"between {min_limit} and {max_limit:,}."
-323                )
-324        else:
-325            # If the value is None
-326            return False
-327
-328        # 3. Check Numeric Range
-329        if min_limit <= numeric_value <= max_limit:
-330            return numeric_value
-331        else:
-332            # Number is out of range
-333            raise ValueError(
-334                f"Invalid numeric value for {param_name}. Must be between {min_limit} and {max_limit:,}. "
-335                f"Received: {numeric_value}"
-336            )
-337
-338    # ------------------------------------------------------
-339    # getZipFile - getting the Zipfile from the SaveFile
-340    # ------------------------------------------------------
-341    def __getSavFile(self,
-342                     localFilePath: str,
-343                     remotePath: str,
-344                     port: int = None
-345                     ) -> bool:
-346        """
-347            Downloads a file from the remote IBM i via SFTP.
-348
-349            This method uses Paramiko to establish a secure shell (SSH) connection and
-350            then an SFTP session to transfer a file from a specified remote location
-351            on the IBM i's IFS to a local path.
-352
-353            Args:
-354                localFilePath (str): The full path to the file on the remote IBM i's IFS.
-355                remotePath (str): The full path on the local machine where the file
-356                                       will be saved. For example, '/Users/user/Documents/somefile.savf'.
-357                port (int, optional): The port to connect to the IBMi server. Defaults to None.
-358
-359            Returns:
-360                bool: True if the file was downloaded successfully, False otherwise.
-361
-362            Raises:
-363                ValueError: If either the remote_file_path or local_save_path is not provided.
-364        """
-365        if not localFilePath:
-366            print("Error: A local file path is required.")
-367            return False
-368        if not remotePath:
-369            print("Error: A remote path is required.")
-370            return False
-371        if not port:
-372            port = 2222
-373
-374        remotePath = PureWindowsPath(remotePath).as_posix()
-375        ssh_client = paramiko.SSHClient()
-376
-377        ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-378
-379        try:
-380            with ssh_client:
-381                ssh_client.connect(
-382                    hostname=self.db_host,
-383                    username=self.db_user,
-384                    password=self.db_password,
-385                    port=port
-386                )
-387                with ssh_client.open_sftp() as ftp_client:
-388                    ftp_client.get(remotePath, localFilePath)
-389                    return True
-390
-391        except paramiko.ssh_exception.AuthenticationException as e:
-392            print(f"Authentication failed. Check your username and password: {e}")
-393            return False
-394        except paramiko.ssh_exception.SSHException as e:
-395            print(f"SSH error occurred: {e}")
-396            return False
-397        except FileNotFoundError as e:
-398            print(f"File not found on the remote host: {e}")
-399            return False
-400
-401        finally:
-402            pass
-403
-404    def removeFile(self, library: str, saveFileName: str) -> bool:
-405        """
-406        Removes a save file from the specified library.
-407
-408        This function executes the system command to delete a save file from an IBM i
-409        system. It connects to the database through a cursor, and attempts to perform
-410        the operation. If an error is encountered during execution, the function
-411        rolls back the transaction and logs the error. On success, the transaction
-412        is committed.
-413
-414        :param library: The name of the library containing the save file to be removed.
-415        :type library: str
-416        :param saveFileName: The name of the save file to be removed.
-417        :type saveFileName: str
-418        :return: True if the save file is removed successfully, otherwise False.
-419        :rtype: bool
-420        """
-421        command_str: str = f"DLTF FILE({library.upper()}/{saveFileName.upper()})"
-422        try:
-423            with self.conn.cursor() as cursor:
-424                # execute the Command for deleting a Savefile.
-425                cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str))
-426
-427        except Exception as e:
-428            self.__handle_error(error=e, pgm="removeFile")
-429            self.conn.rollback()
-430            return False
-431        else:
-432            self.conn.commit()
-433            return True
-434
-435    def __handle_error(self, error, pgm: str):
-436        """
-437        Handles errors encountered during the execution of a command.
-438
-439        This method processes an error raised during the execution of a command in a
-440        specific function and extracts detailed error information including SQLSTATE
-441        and the error message. The formatted details are printed to the console for
-442        debugging purposes.
-443
-444        :param error: The error object encountered during command execution.
-445        :type error: Exception
-446        :param pgm: The name of the function where the error occurred.
-447        :type pgm: str
-448        :return: None
-449        """
-450        print("-------------------------------------------------------------")
-451        print(f"An error occurred while executing command in function {pgm}:")
-452        sqlstate = error.args[0]
-453        error_message = error.args[1]
-454
-455        print(f"SQLSTATE: {sqlstate}")
-456        print(f"Message: {error_message}")
-
- - - - -
- -
- - def - saveLibrary( self, library: str, saveFileName: str, dev: str = None, vol: str = None, toLibrary: str = None, description: str = None, localPath: str = None, remPath: str = None, getZip: bool = False, port: int = None, remSavf=True, version: str = None, max_records: int | str | None = None, asp: int | str | None = None, waitFile: int | str | None = None, share: str = None, authority: str = None) -> bool: - - - -
- -
 15    def saveLibrary(self,
- 16                    library: str,
- 17                    saveFileName: str,
- 18                    dev: str = None,
- 19                    vol: str = None,
- 20                    toLibrary: str = None,
- 21                    description: str = None,
- 22                    localPath: str = None,
- 23                    remPath: str = None,
- 24                    getZip: bool = False,
- 25                    port: int = None,
- 26                    remSavf=True,
- 27                    version: str = None,
- 28                    max_records: Union[int, str, None] = None,
- 29                    asp: Union[int, str, None] = None,
- 30                    waitFile: Union[int, str, None] = None,
- 31                    share: str = None,
- 32                    authority: str = None
- 33                    ) -> bool:
- 34        """
- 35        Saves a library to a specified save file, providing options for further customization such
- 36        as setting the target release, saving as a zip file, specifying the device, volume, and more.
- 37
- 38        :param library: The name of the library to be saved. Must be a valid library name or one of
- 39            the predefined options such as '*NONSYS', '*ALLUSR', '*IBM', etc.
- 40        :type library: str
- 41        :param saveFileName: The name of the save file where the library will be saved.
- 42        :type saveFileName: str
- 43        :param dev: The target device for the save operation. Defaults to '*SAVF' if not provided.
- 44        :type dev: str, optional
- 45        :param vol: Specifies the volume to be used. Use ‘*MOUNTED’ to refer to the mounted volume.
- 46        :type vol: str, optional
- 47        :param toLibrary: Target library where the save file will be temporarily stored. Defaults
- 48            to the value of `library` if not specified.
- 49        :type toLibrary: str, optional
- 50        :param description: An optional description for the save file to be created.
- 51        :type description: str, optional
- 52        :param localPath: The local path where the save file will be downloaded if `getZip` is set
- 53            to True. Must be an absolute path.
- 54        :type localPath: str, optional
- 55        :param remPath: The remote directory path on the target system to temporarily store the
- 56            save file if `getZip` is set to True. Must be an absolute path.
- 57        :type remPath: str, optional
- 58        :param getZip: A flag that determines whether the save file should be archived into a zip
- 59            file and downloaded locally.
- 60        :type getZip: bool
- 61        :param port: Specifies the port to be used for transferring the save file when `getZip` is
- 62            enabled.
- 63        :type port: int, optional
- 64        :param remSavf: A flag indicating whether the save file should be removed from the remote
- 65            target system after a successful save.
- 66        :type remSavf: bool
- 67        :param version: The target release version for the save operation. Valid values include
- 68            ‘*CURRENT’, or specific OS versions like 'V1R1M0', 'V2R3M0', and so on.
- 69        :type version: str, optional
- 70        :param max_records: Optional parameter for specifying the maximum number of records in
- 71            the save file.
- 72        :type max_records: Union[int, str, None], optional
- 73        :param asp: Auxiliary storage pool (ASP) device number or name if applicable.
- 74        :type asp: Union[int, str, None], optional
- 75        :param waitFile: The amount of time to wait for file access locks to be released.
- 76        :type waitFile: Union[int, str, None], optional
- 77        :param share: Specifies the share handling for threads or users accessing the save file.
- 78        :type share: str, optional
- 79        :param authority: Authority option to set for the save file being saved.
- 80        :type authority: str, optional
- 81        :return: A boolean indicating whether the library was successfully saved. Returns True on
- 82            success or False on failure.
- 83        :rtype: bool
- 84        """
- 85        # Target Release List
- 86        trgList: list = ["V1R1M0", "V1R1M2", "V1R2M0", "V1R3M0", "V2R1M0", "V2R1M1",
- 87                         "V2R2M0", "V2R3M0", "V3R0M5", "V3R1M0", "V3R2M0", "V3R6M0",
- 88                         "V3R7M0", "V4R1M0", "V4R2M0", "V4R3M0", "V4R4M0", "V4R5M0",
- 89                         "V5R1M0", "V5R2M0", "V5R3M0", "V5R4M0", "V6R1M0", "V6R1M1",
- 90                         "V7R1M0", "V7R2M0", "V7R3M0", "V7R4M0", "V7R5M0", "V7R6M0"]
- 91
- 92        # check if something missing from the Arguments
- 93        # check if Library is empty or not
- 94        if not library:
- 95            raise ValueError("A library name is required.")
- 96        # check if saveFileName is empty or not
- 97        if not saveFileName:
- 98            raise ValueError("A save file name is required.")
- 99        # check if toLibrary is empty or not
-100        if not toLibrary:
-101            toLibrary = library
-102        # check if user want the SaveFile as ZIP File
-103        if getZip:
-104            if not remPath:
-105                raise ValueError("A remote path is required. Use 'remPath' instead.")
-106            elif remPath[-1] == '/':
-107                remPath = remPath[:-1]
-108            if not localPath:
-109                raise ValueError("A local path is required. Use 'localPath' instead.")
-110            elif localPath[-1] == '/':
-111                localPath = localPath[:-1]
-112        # check wich Version of SaveFile is wanted
-113        if not version in list(trgList):
-114            version = "*CURRENT"
-115        else:
-116            version = version.upper()
-117        command_str: str = f'SAVLIB'
-118
-119        # check if Library is valid or not
-120        validated_library = self.__validate_max_value(value=library, param_name='library',
-121                                                      str_format=['*NONSYS', '*ALLUSR', '*IBM', '*SELECT', '*USRSPC',
-122                                                                  library])
-123        if validated_library:
-124            command_str += f' LIB({validated_library})'
-125        else:
-126            library_str = str(library)
-127            raise ValueError(
-128                f"The library '{library_str}' is not valid. Must be one of the specified strings or a valid number.")
-129        # check Dev - Device
-130        if not dev in ['*SAVF', '*MEDDFN']:
-131            command_str += f' DEV(*SAVF)'
-132        else:
-133            command_str += f' DEV({dev.upper()})'
-134        if vol is not None and vol == '*MOUNTED':
-135            command_str += f' VOL({vol})'
-136        # starting with mem main Sourcecode of saveLLibrary
-137        if self.__crtsavf(saveFileName, toLibrary, description, max_records=max_records, asp=asp, waitFile=waitFile,
-138                          share=share, authority=authority):
-139            # command_str: str = f"SAVLIB LIB({library.strip()}) DEV(*SAVF) SAVF({toLibrary.strip()}/{saveFileName.strip()}) TGTRLS({version.strip()})"
-140            command_str += f" SAVF({toLibrary.strip()}/{saveFileName.strip()}) TGTRLS({version.strip()})"
-141            #print(command_str)
-142            try:
-143                with self.conn.cursor() as cursor:
-144                    # execute the Command for creating a Savefile.
-145                    cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str))
-146                    if getZip:
-147                        try:
-148                            remote_temp_savf_path = join(remPath, saveFileName.upper() + '.savf')
-149
-150                            destination_local_path = join(localPath, saveFileName.upper() + '.savf')
-151                            command_str = (
-152                                f"CPYTOSTMF FROMMBR('/QSYS.LIB/{toLibrary.upper().strip()}.LIB/{saveFileName.upper().strip()}.FILE') "
-153                                f"TOSTMF('{remote_temp_savf_path.strip()}') STMFOPT(*REPLACE)"
-154                            )
-155
-156                            # Execute the command on the remote system
-157                            cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str,))
-158
-159                            if self.__getSavFile(localFilePath=destination_local_path,
-160                                                 remotePath=remote_temp_savf_path, port=port):
-161                                rmvCommand = f"QSH CMD('rm -r {remote_temp_savf_path}')"
-162                                cursor.execute("CALL QSYS2.QCMDEXC(?)", (rmvCommand))
-163                            else:
-164                                raise ValueError("Something went wrong. With downloading the Save File.")
-165                            if remSavf:
-166                                if not self.removeFile(library=toLibrary, saveFileName=saveFileName):
-167                                    raise ValueError(f"The Save File {saveFileName} was not successfully removed.")
-168
-169                        except Exception as e:
-170                            self.__handle_error(error=e, pgm="saveLibrary - Transfer")
-171
-172            except Exception as e:
-173                self.__handle_error(error=e, pgm="saveLibrary")
-174                self.conn.rollback()
-175                return False
-176            else:
-177                self.conn.commit()
-178                if getZip:
-179                    print(f"File successfully downloaded to: {destination_local_path}")
-180                    return True
-181
-182                print(f"Successfully saved in the Library '{library}' successfully.")
-183                return True
-184
-185        return False
-
- - -

Saves a library to a specified save file, providing options for further customization such -as setting the target release, saving as a zip file, specifying the device, volume, and more.

- -
Parameters
- -
    -
  • library: The name of the library to be saved. Must be a valid library name or one of -the predefined options such as '*NONSYS', '*ALLUSR', '*IBM', etc.
  • -
  • saveFileName: The name of the save file where the library will be saved.
  • -
  • dev: The target device for the save operation. Defaults to '*SAVF' if not provided.
  • -
  • vol: Specifies the volume to be used. Use ‘*MOUNTED’ to refer to the mounted volume.
  • -
  • toLibrary: Target library where the save file will be temporarily stored. Defaults -to the value of library if not specified.
  • -
  • description: An optional description for the save file to be created.
  • -
  • localPath: The local path where the save file will be downloaded if getZip is set -to True. Must be an absolute path.
  • -
  • remPath: The remote directory path on the target system to temporarily store the -save file if getZip is set to True. Must be an absolute path.
  • -
  • getZip: A flag that determines whether the save file should be archived into a zip -file and downloaded locally.
  • -
  • port: Specifies the port to be used for transferring the save file when getZip is -enabled.
  • -
  • remSavf: A flag indicating whether the save file should be removed from the remote -target system after a successful save.
  • -
  • version: The target release version for the save operation. Valid values include -‘*CURRENT’, or specific OS versions like 'V1R1M0', 'V2R3M0', and so on.
  • -
  • max_records: Optional parameter for specifying the maximum number of records in -the save file.
  • -
  • asp: Auxiliary storage pool (ASP) device number or name if applicable.
  • -
  • waitFile: The amount of time to wait for file access locks to be released.
  • -
  • share: Specifies the share handling for threads or users accessing the save file.
  • -
  • authority: Authority option to set for the save file being saved.
  • -
- -
Returns
- -
-

A boolean indicating whether the library was successfully saved. Returns True on - success or False on failure.

-
-
- - -
-
- -
- - def - removeFile(self, library: str, saveFileName: str) -> bool: - - - -
- -
404    def removeFile(self, library: str, saveFileName: str) -> bool:
-405        """
-406        Removes a save file from the specified library.
-407
-408        This function executes the system command to delete a save file from an IBM i
-409        system. It connects to the database through a cursor, and attempts to perform
-410        the operation. If an error is encountered during execution, the function
-411        rolls back the transaction and logs the error. On success, the transaction
-412        is committed.
-413
-414        :param library: The name of the library containing the save file to be removed.
-415        :type library: str
-416        :param saveFileName: The name of the save file to be removed.
-417        :type saveFileName: str
-418        :return: True if the save file is removed successfully, otherwise False.
-419        :rtype: bool
-420        """
-421        command_str: str = f"DLTF FILE({library.upper()}/{saveFileName.upper()})"
-422        try:
-423            with self.conn.cursor() as cursor:
-424                # execute the Command for deleting a Savefile.
-425                cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str))
-426
-427        except Exception as e:
-428            self.__handle_error(error=e, pgm="removeFile")
-429            self.conn.rollback()
-430            return False
-431        else:
-432            self.conn.commit()
-433            return True
-
- - -

Removes a save file from the specified library.

- -

This function executes the system command to delete a save file from an IBM i -system. It connects to the database through a cursor, and attempts to perform -the operation. If an error is encountered during execution, the function -rolls back the transaction and logs the error. On success, the transaction -is committed.

- -
Parameters
- -
    -
  • library: The name of the library containing the save file to be removed.
  • -
  • saveFileName: The name of the save file to be removed.
  • -
- -
Returns
- -
-

True if the save file is removed successfully, otherwise False.

-
-
- - -
-
-
- - \ No newline at end of file diff --git a/docs/reference/iLibrary/src/sendMSG.md b/docs/reference/iLibrary/src/sendMSG.md deleted file mode 100644 index 3c4aaea..0000000 --- a/docs/reference/iLibrary/src/sendMSG.md +++ /dev/null @@ -1,497 +0,0 @@ - - - - - - - iLibrary.src.sendMSG API documentation - - - - - - - - - -
-
-

-iLibrary.src.sendMSG

- - - - - - -
 1from os.path import join
- 2import paramiko
- 3import pyodbc
- 4import json
- 5from datetime import datetime, date
- 6from decimal import Decimal
- 7
- 8class sendMSG():
- 9    """
-10    Handles message-related operations by providing functionality to send messages
-11    to specific users within the system. The class interacts with system APIs to
-12    execute the required operations and ensures the input parameters are validated
-13    before proceeding with the message sending process.
-14
-15    Attributes supported by this class are not specified because the class relies
-16    on method-level operations.
-17    """
-18    def send_message_to_user(
-19            self,
-20            username: str,
-21            message: str,
-22            # tomsgq: str = None,
-23            # msgtype: str = None,
-24            # rpymsgq: str = None,
-25            ccsid: int = None
-26    ):
-27        """
-28        Sends a message to a specified user on the system. This method interacts with the system
-29        to send a message by executing an SQL query. It validates the required
-30        inputs and raises an exception if they are missing. Optional parameters for
-31        further message configuration can also be provided.
-32
-33        :param username: The username of the recipient to whom the message will be sent.
-34        :type username: str
-35        :param message: The actual text message to be sent to the user.
-36        :type message: str
-37        :param ccsid: Optional character set identifier (CCSID) for the message. Defaults to None.
-38        :type ccsid: int, optional
-39
-40        :return: None if the message is sent successfully.
-41        :rtype: None
-42
-43        :raises ValueError: If any required parameter, such as `username` or `message`, is missing.
-44
-45        :raises Exception: Any other exceptions that occur during the execution of the
-46            SQL query are raised, indicating issues during the process of sending the
-47            message.
-48        """
-49        if not username:
-50            raise ValueError("Username are required.")
-51        if not message:
-52            raise ValueError("Message are required.")
-53
-54        username = username.upper()
-55        message = message.upper()
-56
-57        sql_query = f"CALL QSYS2.QCMDEXC('SNDMSG MSG(''{message}'') TOUSR({username})')"
-58        if ccsid:
-59            sql_query += f" CCSID({ccsid})"
-60        try:
-61            with self.conn.cursor() as cursor:
-62                cursor.execute(sql_query)
-63                row_dict:dict = {"success": f'Message sent to {username}'}
-64                return json.dumps(row_dict, indent=4)
-65        except Exception as e:
-66            row_dict: dict = {"error" : f'Error:  {e}'}
-67            return json.dumps(row_dict, indent=4)
-
- - -
-
- -
- - class - sendMSG: - - - -
- -
 9class sendMSG():
-10    """
-11    Handles message-related operations by providing functionality to send messages
-12    to specific users within the system. The class interacts with system APIs to
-13    execute the required operations and ensures the input parameters are validated
-14    before proceeding with the message sending process.
-15
-16    Attributes supported by this class are not specified because the class relies
-17    on method-level operations.
-18    """
-19    def send_message_to_user(
-20            self,
-21            username: str,
-22            message: str,
-23            # tomsgq: str = None,
-24            # msgtype: str = None,
-25            # rpymsgq: str = None,
-26            ccsid: int = None
-27    ):
-28        """
-29        Sends a message to a specified user on the system. This method interacts with the system
-30        to send a message by executing an SQL query. It validates the required
-31        inputs and raises an exception if they are missing. Optional parameters for
-32        further message configuration can also be provided.
-33
-34        :param username: The username of the recipient to whom the message will be sent.
-35        :type username: str
-36        :param message: The actual text message to be sent to the user.
-37        :type message: str
-38        :param ccsid: Optional character set identifier (CCSID) for the message. Defaults to None.
-39        :type ccsid: int, optional
-40
-41        :return: None if the message is sent successfully.
-42        :rtype: None
-43
-44        :raises ValueError: If any required parameter, such as `username` or `message`, is missing.
-45
-46        :raises Exception: Any other exceptions that occur during the execution of the
-47            SQL query are raised, indicating issues during the process of sending the
-48            message.
-49        """
-50        if not username:
-51            raise ValueError("Username are required.")
-52        if not message:
-53            raise ValueError("Message are required.")
-54
-55        username = username.upper()
-56        message = message.upper()
-57
-58        sql_query = f"CALL QSYS2.QCMDEXC('SNDMSG MSG(''{message}'') TOUSR({username})')"
-59        if ccsid:
-60            sql_query += f" CCSID({ccsid})"
-61        try:
-62            with self.conn.cursor() as cursor:
-63                cursor.execute(sql_query)
-64                row_dict:dict = {"success": f'Message sent to {username}'}
-65                return json.dumps(row_dict, indent=4)
-66        except Exception as e:
-67            row_dict: dict = {"error" : f'Error:  {e}'}
-68            return json.dumps(row_dict, indent=4)
-
- - -

Handles message-related operations by providing functionality to send messages -to specific users within the system. The class interacts with system APIs to -execute the required operations and ensures the input parameters are validated -before proceeding with the message sending process.

- -

Attributes supported by this class are not specified because the class relies -on method-level operations.

-
- - -
- -
- - def - send_message_to_user(self, username: str, message: str, ccsid: int = None): - - - -
- -
19    def send_message_to_user(
-20            self,
-21            username: str,
-22            message: str,
-23            # tomsgq: str = None,
-24            # msgtype: str = None,
-25            # rpymsgq: str = None,
-26            ccsid: int = None
-27    ):
-28        """
-29        Sends a message to a specified user on the system. This method interacts with the system
-30        to send a message by executing an SQL query. It validates the required
-31        inputs and raises an exception if they are missing. Optional parameters for
-32        further message configuration can also be provided.
-33
-34        :param username: The username of the recipient to whom the message will be sent.
-35        :type username: str
-36        :param message: The actual text message to be sent to the user.
-37        :type message: str
-38        :param ccsid: Optional character set identifier (CCSID) for the message. Defaults to None.
-39        :type ccsid: int, optional
-40
-41        :return: None if the message is sent successfully.
-42        :rtype: None
-43
-44        :raises ValueError: If any required parameter, such as `username` or `message`, is missing.
-45
-46        :raises Exception: Any other exceptions that occur during the execution of the
-47            SQL query are raised, indicating issues during the process of sending the
-48            message.
-49        """
-50        if not username:
-51            raise ValueError("Username are required.")
-52        if not message:
-53            raise ValueError("Message are required.")
-54
-55        username = username.upper()
-56        message = message.upper()
-57
-58        sql_query = f"CALL QSYS2.QCMDEXC('SNDMSG MSG(''{message}'') TOUSR({username})')"
-59        if ccsid:
-60            sql_query += f" CCSID({ccsid})"
-61        try:
-62            with self.conn.cursor() as cursor:
-63                cursor.execute(sql_query)
-64                row_dict:dict = {"success": f'Message sent to {username}'}
-65                return json.dumps(row_dict, indent=4)
-66        except Exception as e:
-67            row_dict: dict = {"error" : f'Error:  {e}'}
-68            return json.dumps(row_dict, indent=4)
-
- - -

Sends a message to a specified user on the system. This method interacts with the system -to send a message by executing an SQL query. It validates the required -inputs and raises an exception if they are missing. Optional parameters for -further message configuration can also be provided.

- -
Parameters
- -
    -
  • username: The username of the recipient to whom the message will be sent.
  • -
  • message: The actual text message to be sent to the user.
  • -
  • ccsid: Optional character set identifier (CCSID) for the message. Defaults to None.
  • -
- -
Returns
- -
-

None if the message is sent successfully.

-
- -
Raises
- -
    -
  • ValueError: If any required parameter, such as username or message, is missing.

  • -
  • Exception: Any other exceptions that occur during the execution of the -SQL query are raised, indicating issues during the process of sending the -message.

  • -
-
- - -
-
-
- - \ No newline at end of file diff --git a/docs/reference/index.md b/docs/reference/index.md deleted file mode 100644 index defe329..0000000 --- a/docs/reference/index.md +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/docs/reference/search.js b/docs/reference/search.js deleted file mode 100644 index 0eca9e9..0000000 --- a/docs/reference/search.js +++ /dev/null @@ -1,46 +0,0 @@ -window.pdocSearch = (function(){ -/** elasticlunr - http://weixsong.github.io * Copyright (C) 2017 Oliver Nightingale * Copyright (C) 2017 Wei Song * MIT Licensed */!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o

\n"}, {"fullname": "iLibrary.src.Library", "modulename": "iLibrary.src.Library", "kind": "module", "doc": "

\n"}, {"fullname": "iLibrary.src.Library.Library", "modulename": "iLibrary.src.Library", "qualname": "Library", "kind": "class", "doc": "

A class to manage libraries and files on an IBM i system.

\n\n

It provides methods to connect to the system via pyodbc for SQL and\nparamiko for SFTP transfers.

\n", "bases": "iLibrary.src.getInfoForLibrary.getInfoForLibrary, iLibrary.src.saveLibrary.saveLibrary"}, {"fullname": "iLibrary.src.Library.Library.__init__", "modulename": "iLibrary.src.Library", "qualname": "Library.__init__", "kind": "function", "doc": "

Initializes the class attributes for a database connection.\nThe actual connection is established in the __enter__ method.

\n\n

Args:\n db_user (str): The user ID for the database connection.\n db_password (str): The password for the database user.\n db_host (str): The system/host name for the database connection.\n db_driver (str): The ODBC driver to be used.

\n", "signature": "(db_user: str, db_password: str, db_host: str, db_driver: str)"}, {"fullname": "iLibrary.src.Library.Library.db_user", "modulename": "iLibrary.src.Library", "qualname": "Library.db_user", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.Library.Library.db_host", "modulename": "iLibrary.src.Library", "qualname": "Library.db_host", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.Library.Library.db_driver", "modulename": "iLibrary.src.Library", "qualname": "Library.db_driver", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.Library.Library.db_password", "modulename": "iLibrary.src.Library", "qualname": "Library.db_password", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.Library.Library.iclose", "modulename": "iLibrary.src.Library", "qualname": "Library.iclose", "kind": "function", "doc": "

A helper method to close the connection, also useful for manual closure.

\n", "signature": "(self):", "funcdef": "def"}, {"fullname": "iLibrary.src.User", "modulename": "iLibrary.src.User", "kind": "module", "doc": "

\n"}, {"fullname": "iLibrary.src.User.User", "modulename": "iLibrary.src.User", "qualname": "User", "kind": "class", "doc": "

A class to manage User on IBMi System

\n\n

It provides methods to connect to the system via pyodbc for SQL and\nparamiko for SFTP transfers.

\n", "bases": "iLibrary.src.getUserInfoForUser.getUserInfoForUser, iLibrary.src.sendMSG.sendMSG"}, {"fullname": "iLibrary.src.User.User.__init__", "modulename": "iLibrary.src.User", "qualname": "User.__init__", "kind": "function", "doc": "

Initializes the class attributes for a database connection.\nThe actual connection is established in the __enter__ method.

\n\n

Args:\n db_user (str): The user ID for the database connection.\n db_password (str): The password for the database user.\n db_host (str): The system/host name for the database connection.\n db_driver (str): The ODBC driver to be used.

\n", "signature": "(db_user: str, db_password: str, db_host: str, db_driver: str)"}, {"fullname": "iLibrary.src.User.User.db_user", "modulename": "iLibrary.src.User", "qualname": "User.db_user", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.User.User.db_host", "modulename": "iLibrary.src.User", "qualname": "User.db_host", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.User.User.db_driver", "modulename": "iLibrary.src.User", "qualname": "User.db_driver", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.User.User.db_password", "modulename": "iLibrary.src.User", "qualname": "User.db_password", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.User.User.iclose", "modulename": "iLibrary.src.User", "qualname": "User.iclose", "kind": "function", "doc": "

A helper method to close the connection, also useful for manual closure.

\n", "signature": "(self):", "funcdef": "def"}, {"fullname": "iLibrary.src.getInfoForLibrary", "modulename": "iLibrary.src.getInfoForLibrary", "kind": "module", "doc": "

\n"}, {"fullname": "iLibrary.src.getInfoForLibrary.getInfoForLibrary", "modulename": "iLibrary.src.getInfoForLibrary", "qualname": "getInfoForLibrary", "kind": "class", "doc": "

\n"}, {"fullname": "iLibrary.src.getInfoForLibrary.getInfoForLibrary.__init__", "modulename": "iLibrary.src.getInfoForLibrary", "qualname": "getInfoForLibrary.__init__", "kind": "function", "doc": "

\n", "signature": "(connection)"}, {"fullname": "iLibrary.src.getInfoForLibrary.getInfoForLibrary.conn", "modulename": "iLibrary.src.getInfoForLibrary", "qualname": "getInfoForLibrary.conn", "kind": "variable", "doc": "

\n"}, {"fullname": "iLibrary.src.getInfoForLibrary.getInfoForLibrary.getLibraryInfo", "modulename": "iLibrary.src.getInfoForLibrary", "qualname": "getInfoForLibrary.getLibraryInfo", "kind": "function", "doc": "

\n", "signature": "(self, library: str, wantJson=True):", "funcdef": "def"}, {"fullname": "iLibrary.src.getInfoForLibrary.getInfoForLibrary.getFileInfo", "modulename": "iLibrary.src.getInfoForLibrary", "qualname": "getInfoForLibrary.getFileInfo", "kind": "function", "doc": "

\n", "signature": "(self, library: str, qFiles: bool = False) -> str:", "funcdef": "def"}, {"fullname": "iLibrary.src.getInfoForLibrary.getInfoForLibrary.getAllLibraries", "modulename": "iLibrary.src.getInfoForLibrary", "qualname": "getInfoForLibrary.getAllLibraries", "kind": "function", "doc": "

\n", "signature": "(self):", "funcdef": "def"}, {"fullname": "iLibrary.src.getUserInfoForUser", "modulename": "iLibrary.src.getUserInfoForUser", "kind": "module", "doc": "

\n"}, {"fullname": "iLibrary.src.getUserInfoForUser.getUserInfoForUser", "modulename": "iLibrary.src.getUserInfoForUser", "qualname": "getUserInfoForUser", "kind": "class", "doc": "

Handles user information retrieval and messaging functionalities.

\n\n

This class provides methods to interact with the database for retrieving user information\nand to send messages to specified users. It supports data retrieval in different formats\n(e.g., JSON or tuple), and it enables system messaging with configurable options.

\n\n

:ivar conn: Database connection object used for executing queries.

\n"}, {"fullname": "iLibrary.src.getUserInfoForUser.getUserInfoForUser.getAllUsers", "modulename": "iLibrary.src.getUserInfoForUser", "qualname": "getUserInfoForUser.getAllUsers", "kind": "function", "doc": "

Retrieves all user information from the database. Optionally returns the data in\nJSON format depending on the provided parameter.

\n\n

Retrieves a list of users stored in the database and can output the data either as\na list of tuples or in JSON format. The query fetches all fields available in the\nuser information database table and handles cases where no data is found.

\n\n
Parameters
\n\n
    \n
  • wantJson: Boolean flag to indicate whether the result should be returned\nin JSON format. If set to False, the result will be a list of tuples. Default\nis False.
  • \n
\n\n
Returns
\n\n
\n

The data fetched from the database. When wantJson is True, returns a\n JSON object as a string. Otherwise, returns a list of tuples.

\n
\n", "signature": "(self, wantJson: bool = False):", "funcdef": "def"}, {"fullname": "iLibrary.src.getUserInfoForUser.getUserInfoForUser.getSingleUserInformation", "modulename": "iLibrary.src.getUserInfoForUser", "qualname": "getUserInfoForUser.getSingleUserInformation", "kind": "function", "doc": "

Retrieves information about a specific user from the database based on their username. The function supports\nreturning data either as a JSON-formatted string or as a tuple with corresponding database fields.

\n\n
Parameters
\n\n
    \n
  • username: The username of the database user whose information is to be retrieved. Must not be empty.
  • \n
  • wantJson: Indicates whether the output should be formatted as JSON. Defaults to False.
  • \n
\n\n
Returns
\n\n
\n

A tuple containing database fields if wantJson is False, or a JSON-formatted string if wantJson is True.\n If no user is found, returns either a JSON-formatted error message or a tuple with error details, based on the\n value of wantJson. Returns None if an exception occurs.

\n
\n\n
Raises
\n\n
    \n
  • ValueError: If the username input is empty or None.
  • \n
\n", "signature": "(self, username: str, wantJson: bool = False):", "funcdef": "def"}, {"fullname": "iLibrary.src.saveLibrary", "modulename": "iLibrary.src.saveLibrary", "kind": "module", "doc": "

\n"}, {"fullname": "iLibrary.src.saveLibrary.saveLibrary", "modulename": "iLibrary.src.saveLibrary", "qualname": "saveLibrary", "kind": "class", "doc": "

\n"}, {"fullname": "iLibrary.src.saveLibrary.saveLibrary.saveLibrary", "modulename": "iLibrary.src.saveLibrary", "qualname": "saveLibrary.saveLibrary", "kind": "function", "doc": "

Saves a library to a specified save file, providing options for further customization such\nas setting the target release, saving as a zip file, specifying the device, volume, and more.

\n\n
Parameters
\n\n
    \n
  • library: The name of the library to be saved. Must be a valid library name or one of\nthe predefined options such as '*NONSYS', '*ALLUSR', '*IBM', etc.
  • \n
  • saveFileName: The name of the save file where the library will be saved.
  • \n
  • dev: The target device for the save operation. Defaults to '*SAVF' if not provided.
  • \n
  • vol: Specifies the volume to be used. Use \u2018*MOUNTED\u2019 to refer to the mounted volume.
  • \n
  • toLibrary: Target library where the save file will be temporarily stored. Defaults\nto the value of library if not specified.
  • \n
  • description: An optional description for the save file to be created.
  • \n
  • localPath: The local path where the save file will be downloaded if getZip is set\nto True. Must be an absolute path.
  • \n
  • remPath: The remote directory path on the target system to temporarily store the\nsave file if getZip is set to True. Must be an absolute path.
  • \n
  • getZip: A flag that determines whether the save file should be archived into a zip\nfile and downloaded locally.
  • \n
  • port: Specifies the port to be used for transferring the save file when getZip is\nenabled.
  • \n
  • remSavf: A flag indicating whether the save file should be removed from the remote\ntarget system after a successful save.
  • \n
  • version: The target release version for the save operation. Valid values include\n\u2018*CURRENT\u2019, or specific OS versions like 'V1R1M0', 'V2R3M0', and so on.
  • \n
  • max_records: Optional parameter for specifying the maximum number of records in\nthe save file.
  • \n
  • asp: Auxiliary storage pool (ASP) device number or name if applicable.
  • \n
  • waitFile: The amount of time to wait for file access locks to be released.
  • \n
  • share: Specifies the share handling for threads or users accessing the save file.
  • \n
  • authority: Authority option to set for the save file being saved.
  • \n
\n\n
Returns
\n\n
\n

A boolean indicating whether the library was successfully saved. Returns True on\n success or False on failure.

\n
\n", "signature": "(\tself,\tlibrary: str,\tsaveFileName: str,\tdev: str = None,\tvol: str = None,\ttoLibrary: str = None,\tdescription: str = None,\tlocalPath: str = None,\tremPath: str = None,\tgetZip: bool = False,\tport: int = None,\tremSavf=True,\tversion: str = None,\tmax_records: int | str | None = None,\tasp: int | str | None = None,\twaitFile: int | str | None = None,\tshare: str = None,\tauthority: str = None) -> bool:", "funcdef": "def"}, {"fullname": "iLibrary.src.saveLibrary.saveLibrary.removeFile", "modulename": "iLibrary.src.saveLibrary", "qualname": "saveLibrary.removeFile", "kind": "function", "doc": "

Removes a save file from the specified library.

\n\n

This function executes the system command to delete a save file from an IBM i\nsystem. It connects to the database through a cursor, and attempts to perform\nthe operation. If an error is encountered during execution, the function\nrolls back the transaction and logs the error. On success, the transaction\nis committed.

\n\n
Parameters
\n\n
    \n
  • library: The name of the library containing the save file to be removed.
  • \n
  • saveFileName: The name of the save file to be removed.
  • \n
\n\n
Returns
\n\n
\n

True if the save file is removed successfully, otherwise False.

\n
\n", "signature": "(self, library: str, saveFileName: str) -> bool:", "funcdef": "def"}, {"fullname": "iLibrary.src.sendMSG", "modulename": "iLibrary.src.sendMSG", "kind": "module", "doc": "

\n"}, {"fullname": "iLibrary.src.sendMSG.sendMSG", "modulename": "iLibrary.src.sendMSG", "qualname": "sendMSG", "kind": "class", "doc": "

Handles message-related operations by providing functionality to send messages\nto specific users within the system. The class interacts with system APIs to\nexecute the required operations and ensures the input parameters are validated\nbefore proceeding with the message sending process.

\n\n

Attributes supported by this class are not specified because the class relies\non method-level operations.

\n"}, {"fullname": "iLibrary.src.sendMSG.sendMSG.send_message_to_user", "modulename": "iLibrary.src.sendMSG", "qualname": "sendMSG.send_message_to_user", "kind": "function", "doc": "

Sends a message to a specified user on the system. This method interacts with the system\nto send a message by executing an SQL query. It validates the required\ninputs and raises an exception if they are missing. Optional parameters for\nfurther message configuration can also be provided.

\n\n
Parameters
\n\n
    \n
  • username: The username of the recipient to whom the message will be sent.
  • \n
  • message: The actual text message to be sent to the user.
  • \n
  • ccsid: Optional character set identifier (CCSID) for the message. Defaults to None.
  • \n
\n\n
Returns
\n\n
\n

None if the message is sent successfully.

\n
\n\n
Raises
\n\n
    \n
  • ValueError: If any required parameter, such as username or message, is missing.

  • \n
  • Exception: Any other exceptions that occur during the execution of the\nSQL query are raised, indicating issues during the process of sending the\nmessage.

  • \n
\n", "signature": "(self, username: str, message: str, ccsid: int = None):", "funcdef": "def"}]; - - // mirrored in build-search-index.js (part 1) - // Also split on html tags. this is a cheap heuristic, but good enough. - elasticlunr.tokenizer.setSeperator(/[\s\-.;&_'"=,()]+|<[^>]*>/); - - let searchIndex; - if (docs._isPrebuiltIndex) { - console.info("using precompiled search index"); - searchIndex = elasticlunr.Index.load(docs); - } else { - console.time("building search index"); - // mirrored in build-search-index.js (part 2) - searchIndex = elasticlunr(function () { - this.pipeline.remove(elasticlunr.stemmer); - this.pipeline.remove(elasticlunr.stopWordFilter); - this.addField("qualname"); - this.addField("fullname"); - this.addField("annotation"); - this.addField("default_value"); - this.addField("signature"); - this.addField("bases"); - this.addField("doc"); - this.setRef("fullname"); - }); - for (let doc of docs) { - searchIndex.addDoc(doc); - } - console.timeEnd("building search index"); - } - - return (term) => searchIndex.search(term, { - fields: { - qualname: {boost: 4}, - fullname: {boost: 2}, - annotation: {boost: 2}, - default_value: {boost: 2}, - signature: {boost: 2}, - bases: {boost: 2}, - doc: {boost: 1}, - }, - expand: true - }); -})(); \ No newline at end of file From 047f72dd0d0ad2a29b29722b99e6854e03a08db3 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 20:56:09 +0100 Subject: [PATCH 22/31] fixxes Docs Test --- mkdocs.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 mkdocs.yml diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..ee872cf --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,11 @@ +site_name: "iLibrary Documentation" +site_url: https://legnerbeer.github.io/iLibrary/ + +theme: + name: material # You already have this installed via your logs! + +# This is the most important part to fix the 404 +nav: + - Home: index.md + - API Reference: + - reference/index.md # This points to the folder your script created \ No newline at end of file From 5adfb80ceeaa56d0de9fd405124653ba43137961 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 21:08:30 +0100 Subject: [PATCH 23/31] fixxes Docs Test --- mkdocs.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index ee872cf..47a723c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,4 +8,7 @@ theme: nav: - Home: index.md - API Reference: - - reference/index.md # This points to the folder your script created \ No newline at end of file + - Library: iLibrary/src/Library.md + - User: iLibrary/src/User.md + - Save: iLibrary/src/saveLibrary.md + # Add the others here following the same pattern From 671c92f60b5b16df82c7d8557f8c211e8df94d52 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 21:10:11 +0100 Subject: [PATCH 24/31] fixxes Docs Test --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 47a723c..f354f55 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,7 +4,7 @@ site_url: https://legnerbeer.github.io/iLibrary/ theme: name: material # You already have this installed via your logs! -# This is the most important part to fix the 404 + nav: - Home: index.md - API Reference: From 8aedd97bf535bf709153582aaff75695d1d8687b Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 21:16:06 +0100 Subject: [PATCH 25/31] fixxes Docs Test --- .github/workflows/docs.yml | 2 +- docs/makedoc.py | 3 --- mkdocs.yml | 3 ++- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4b84734..df71bb7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: - name: Install dependencies run: | pip install -e . - pip install pdoc mkdocs-material + pip install pdoc mkdocs # Note: mkdocs-material is the standard theme, add any other plugins you use here. - name: Generate API Reference diff --git a/docs/makedoc.py b/docs/makedoc.py index 9fc69b7..6817009 100644 --- a/docs/makedoc.py +++ b/docs/makedoc.py @@ -29,9 +29,6 @@ def generate_docs(): # pdoc will crawl the src_folder and generate HTML by default pdoc(src_folder, output_directory=out) - # 3. Optional: Rename for MkDocs if you are using the MkDocs-Material logic - # for f in out.glob("**/*.html"): - # f.rename(f.with_suffix(".md")) # Rename .html to .md for f in out.glob("**/*.html"): f.rename(f.with_suffix(".md")) diff --git a/mkdocs.yml b/mkdocs.yml index f354f55..4b77f62 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,6 @@ site_name: "iLibrary Documentation" -site_url: https://legnerbeer.github.io/iLibrary/ +site_url: https://legnerbeer.github.io/iLibrary/ # <--- MUST HAVE THE TRAILING SLASH +use_directory_urls: false theme: name: material # You already have this installed via your logs! From e9d0ae0be065c876dabfa18bcf00bc271edfdeae Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 21:20:00 +0100 Subject: [PATCH 26/31] fixxes Docs Test --- .github/workflows/docs.yml | 8 +------- mkdocs.yml | 10 +++++----- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index df71bb7..f7b560e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,18 +1,15 @@ name: website -# build the documentation whenever there are new commits on main on: push: branches: - main - Developer -# security: restrict permissions for CI jobs. permissions: contents: read jobs: - # Build the documentation and upload the static HTML files as an artifact. build: runs-on: ubuntu-latest steps: @@ -27,8 +24,7 @@ jobs: - name: Install dependencies run: | pip install -e . - pip install pdoc mkdocs - # Note: mkdocs-material is the standard theme, add any other plugins you use here. + pip install pdoc mkdocs # Only installs the core mkdocs - name: Generate API Reference run: python docs/makedoc.py @@ -41,8 +37,6 @@ jobs: with: path: site/ - # Deploy the artifact to GitHub pages. - # This is a separate job so that only actions/deploy-pages has the necessary permissions. deploy: needs: build runs-on: ubuntu-latest diff --git a/mkdocs.yml b/mkdocs.yml index 4b77f62..535d646 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,10 +1,8 @@ site_name: "iLibrary Documentation" -site_url: https://legnerbeer.github.io/iLibrary/ # <--- MUST HAVE THE TRAILING SLASH -use_directory_urls: false +site_url: https://legnerbeer.github.io/iLibrary/ theme: - name: material # You already have this installed via your logs! - + name: mkdocs # This is the built-in default theme nav: - Home: index.md @@ -12,4 +10,6 @@ nav: - Library: iLibrary/src/Library.md - User: iLibrary/src/User.md - Save: iLibrary/src/saveLibrary.md - # Add the others here following the same pattern + - Info (Library): iLibrary/src/getInfoForLibrary.md + - Info (User): iLibrary/src/getUserInfoForUser.md + - Message: iLibrary/src/sendMSG.md \ No newline at end of file From dddb919219c3ad6a080662eaccb86505749e50c7 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 21:34:07 +0100 Subject: [PATCH 27/31] fixxes Docs Test --- .github/workflows/docs.yml | 41 ++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f7b560e..3087fb3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,50 +2,47 @@ name: website on: push: - branches: - - main - - Developer + branches: ["main", "master", "Developer"] permissions: contents: read + pages: write + id-token: write jobs: - build: + build_and_deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - with: - persist-credentials: false + - name: Checkout Code + uses: actions/checkout@v4 - - uses: actions/setup-python@v6 + # --- STEP 1: RUN YOUR PYTHON SCRIPT --- + - name: Set up Python + uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install dependencies run: | pip install -e . - pip install pdoc mkdocs # Only installs the core mkdocs + pip install pdoc mkdocs - name: Generate API Reference run: python docs/makedoc.py + # --- STEP 2: LINK TO MKDOCS BUILD --- - name: Build Static Website run: mkdocs build + # --- STEP 3: LINK TO THE WORKING DEPLOYMENT --- + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload Page Artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@v3 with: - path: site/ + path: ./site # This folder was created by Step 2 - deploy: - needs: build - runs-on: ubuntu-latest - permissions: - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - id: deployment + - name: Deploy to GitHub Pages + id: deployment uses: actions/deploy-pages@v4 \ No newline at end of file From 0350ea48d8e1ba4f23a15a301686ab799b032913 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Sun, 8 Mar 2026 22:01:36 +0100 Subject: [PATCH 28/31] fixxes Docs Test --- .github/workflows/docs.yml | 48 -------------------------------------- docs/makedoc.py | 36 ---------------------------- mkdocs.yml | 15 ------------ 3 files changed, 99 deletions(-) delete mode 100644 .github/workflows/docs.yml delete mode 100644 docs/makedoc.py delete mode 100644 mkdocs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 3087fb3..0000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: website - -on: - push: - branches: ["main", "master", "Developer"] - -permissions: - contents: read - pages: write - id-token: write - -jobs: - build_and_deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v4 - - # --- STEP 1: RUN YOUR PYTHON SCRIPT --- - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Install dependencies - run: | - pip install -e . - pip install pdoc mkdocs - - - name: Generate API Reference - run: python docs/makedoc.py - - # --- STEP 2: LINK TO MKDOCS BUILD --- - - name: Build Static Website - run: mkdocs build - - # --- STEP 3: LINK TO THE WORKING DEPLOYMENT --- - - name: Setup Pages - uses: actions/configure-pages@v5 - - - name: Upload Page Artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./site # This folder was created by Step 2 - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/docs/makedoc.py b/docs/makedoc.py deleted file mode 100644 index 6817009..0000000 --- a/docs/makedoc.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -from pathlib import Path -import shutil -from pdoc import pdoc - -# 1. Define paths relative to this script -# .parent points to the folder containing this script (e.g., your 'docs_scripts' folder) -script_dir = Path(__file__).parent.resolve() - -# Define the root of your GitHub repo (one level up from the script folder) -root_path = script_dir.parent - -# Path to your source code: root/src -src_folder = root_path / "app" / "iLibrary" /"src" - -# Path to the output: root/docs/reference -out = root_path / "docs" / "reference" - -def generate_docs(): - # Clean up old documentation - if out.exists(): - print(f"Cleaning up old docs at: {out}") - shutil.rmtree(out) - - print(f"Generating docs from: {src_folder}") - print(f"Outputting to: {out}") - - # 2. Generate for your project - # pdoc will crawl the src_folder and generate HTML by default - pdoc(src_folder, output_directory=out) - - # Rename .html to .md - for f in out.glob("**/*.html"): - f.rename(f.with_suffix(".md")) -if __name__ == "__main__": - generate_docs() \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 1e5bc37..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,15 +0,0 @@ -site_name: "iLibrary Documentation" -site_url: https://legnerbeer.github.io/iLibrary/ - -theme: - name: mkdocs # This is the built-in default theme - -nav: - - Home: index.md - - API Reference: - - Library: iLibrary/src/Library.md - - User: iLibrary/src/User.md - - Save: iLibrary/src/saveLibrary.md - - Info (Library): iLibrary/src/getInfoForLibrary.md - - Info (User): iLibrary/src/getUserInfoForUser.md - - Message: iLibrary/src/sendMSG.md From ff1cf3c7e17a621cdfce2d9c0c8051fd58117bde Mon Sep 17 00:00:00 2001 From: Andy Date: Sat, 18 Apr 2026 18:10:49 +0200 Subject: [PATCH 29/31] New Update (#15) * Developer (#9) * Deploy to 0.0.17 --- .github/workflows/python-package.yml | 7 +- .gitignore | 2 +- Examples/Readme.md | 310 ++++++++++++ Examples/example.env | 4 + Examples/example_ifs.py | 54 +++ Examples/example_library.py | 124 +++++ Examples/example_user.py | 119 +++++ README.md | 152 ++++-- app/iLibrary/__init__.py | 6 - app/iLibrary/src/getInfoForLibrary.py | 94 ---- app/iLibrary/src/iLibrary/IFS.py | 94 ++++ .../src/{ => iLibrary/Libr}/__init__.py | 0 .../src/iLibrary/Libr/getInfoForLibrary.py | 155 ++++++ app/iLibrary/src/iLibrary/Libr/saveLibrary.py | 198 ++++++++ app/iLibrary/src/{ => iLibrary}/Library.py | 58 ++- app/iLibrary/src/{ => iLibrary}/User.py | 60 ++- app/iLibrary/src/iLibrary/Usr/__init__.py | 0 .../{ => iLibrary/Usr}/getUserInfoForUser.py | 58 ++- .../src/{ => iLibrary/Usr}/sendMSG.py | 29 +- app/iLibrary/src/iLibrary/__init__.py | 3 + app/iLibrary/src/iLibrary/ifs/__init__.py | 0 app/iLibrary/src/iLibrary/ifs/ifs_logic.py | 53 ++ .../src/iLibrary/util_functions/__init__.py | 1 + .../src/iLibrary/util_functions/helper.py | 34 ++ app/iLibrary/src/saveLibrary.py | 455 ------------------ app/iLibrary/tests/test_user.py | 87 ++++ dev_test.py | 18 + requirements.txt | 2 +- setup.cfg | 4 +- setup.py | 7 +- test.py | 34 -- 31 files changed, 1506 insertions(+), 716 deletions(-) create mode 100644 Examples/Readme.md create mode 100644 Examples/example.env create mode 100644 Examples/example_ifs.py create mode 100644 Examples/example_library.py create mode 100644 Examples/example_user.py delete mode 100644 app/iLibrary/__init__.py delete mode 100644 app/iLibrary/src/getInfoForLibrary.py create mode 100644 app/iLibrary/src/iLibrary/IFS.py rename app/iLibrary/src/{ => iLibrary/Libr}/__init__.py (100%) create mode 100644 app/iLibrary/src/iLibrary/Libr/getInfoForLibrary.py create mode 100644 app/iLibrary/src/iLibrary/Libr/saveLibrary.py rename app/iLibrary/src/{ => iLibrary}/Library.py (62%) rename app/iLibrary/src/{ => iLibrary}/User.py (62%) create mode 100644 app/iLibrary/src/iLibrary/Usr/__init__.py rename app/iLibrary/src/{ => iLibrary/Usr}/getUserInfoForUser.py (68%) rename app/iLibrary/src/{ => iLibrary/Usr}/sendMSG.py (72%) create mode 100644 app/iLibrary/src/iLibrary/__init__.py create mode 100644 app/iLibrary/src/iLibrary/ifs/__init__.py create mode 100644 app/iLibrary/src/iLibrary/ifs/ifs_logic.py create mode 100644 app/iLibrary/src/iLibrary/util_functions/__init__.py create mode 100644 app/iLibrary/src/iLibrary/util_functions/helper.py delete mode 100644 app/iLibrary/src/saveLibrary.py create mode 100644 app/iLibrary/tests/test_user.py create mode 100644 dev_test.py delete mode 100644 test.py diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index a644e67..e765c25 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -2,9 +2,9 @@ name: iLibrary on: push: - branches: [ "master" ] + branches: [ "**" ] pull_request: - branches: [ "master" ] + branches: [ "**" ] jobs: build: @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.12", "3.13", "3.14"] # 1. Inject SECRETS as Environment Variables for the entire job env: @@ -52,6 +52,7 @@ jobs: python -m pip install --upgrade pip # pyodbc is needed for the IBM i connection python -m pip install -r requirements.txt + python -m pip install -e . python -m pip install flake8 pytest # Ensure testing/linting tools are installed - name: Lint with flake8 diff --git a/.gitignore b/.gitignore index 5ab2c55..a8664c6 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ __pycache__/ # C extensions *.so - +.DS_Store # Distribution / packaging .Python build/ diff --git a/Examples/Readme.md b/Examples/Readme.md new file mode 100644 index 0000000..62c1576 --- /dev/null +++ b/Examples/Readme.md @@ -0,0 +1,310 @@ +## More Examples + +**Library:** +```python +import json +from os.path import join +import os +from dotenv import load_dotenv +from iLibrary import Library + +#load ENV file and get the Connection Settings +dotenv_path = join('.env') +load_dotenv(dotenv_path) +DB_DRIVER = os.environ.get("DB_DRIVER") +DB_USER = os.environ.get("DB_USER") +DB_PASSWORD = os.environ.get("DB_PASSWORD") +DB_SYSTEM = os.environ.get("DB_SYSTEM") + + + + +# ---------------------------------------------------- +# make a Savefile from a library +# ---------------------------------------------------- +def getSaveFile(): + + # Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE = False + + + PORT = 22 #for the Pub400.com use the Port 2222 + LIBRARY = '' + SAVE_FILE_NAME = 'FOO' + LOCAL_PATH = 'YOUR_LOCAL_PATH' + DESCRIPTION = 'Saved from iLibrary' + try: + # Establish a connection to the IBM i system using the User class + # The context manager ensures the connection is properly opened and closed + with Library(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as l: + + # Call the method to save the Library + # The result is returned as a JSON string + raw_result = l.saveLibrary( + library=LIBRARY, + saveFileName=SAVE_FILE_NAME, + description=DESCRIPTION, + localPath=LOCAL_PATH, + port=PORT + ) + + # Parse the JSON string into a Python object (list/dictionary) + data = json.loads(raw_result) + + # Pretty-print the parsed data with indentation for readability + print(json.dumps(data, indent=4)) + + # Handle any exceptions that occur during connection or data retrieval + except Exception as e: + # Print the error message for debugging + print(e) + + + +# ---------------------------------------------------- +# Get library information about all libraries from +# IBM i Server using iLibrary +# ---------------------------------------------------- +def getAllLibraries(): + # Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE = False + + try: + # Establish a connection to the IBM i system using the User class + # The context manager ensures the connection is properly opened and closed + with Library(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as l: + + # Call the method to get all libraries on the system + # The result is returned as a JSON string + raw_result = l.getAllLibraries() + + # Parse the JSON string into a Python object (list/dictionary) + data = json.loads(raw_result) + + # Pretty-print the parsed data with indentation for readability + print(json.dumps(data, indent=4)) + + # Handle any exceptions that occur during connection or data retrieval + except Exception as e: + # Print the error message for debugging + print(e) + + + + +# ---------------------------------------------------- +# Get single library information from IBM i Server +# using iLibrary +# ---------------------------------------------------- +def getSingleLibraryInfo(): + # Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE = False + LIBRARY = '' + try: + # Establish a connection to the IBM i system using the User class + # The context manager ensures the connection is properly opened and closed + with Library(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as l: + + # Call the method to show singe information about the Library + # The result is returned as a JSON string + raw_result = l.getLibraryInfo( + library=LIBRARY + ) + + # Parse the JSON string into a Python object (list/dictionary) + data = json.loads(raw_result) + + # Pretty-print the parsed data with indentation for readability + print(json.dumps(data, indent=4)) + + # Handle any exceptions that occur during connection or data retrieval + except Exception as e: + # Print the error message for debugging + print(e) + +if __name__ == '__main__': + getSaveFile() + getAllLibraries() + getSingleLibraryInfo() +``` + +**User:** +```python + +import json +from os.path import join +import os +from dotenv import load_dotenv +from iLibrary import User + +#load ENV file and get the Connection Settings +dotenv_path = join('.env') +load_dotenv(dotenv_path) +DB_DRIVER = os.environ.get("DB_DRIVER") +DB_USER = os.environ.get("DB_USER") +DB_PASSWORD = os.environ.get("DB_PASSWORD") +DB_SYSTEM = os.environ.get("DB_SYSTEM") + +# ---------------------------------------------------- +# Get all Users information from IBM i Server using +# iLibrary +# ---------------------------------------------------- + +def getAllUsers(): +# Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE = False + + try: + # Establish a connection to the IBM i system using the User class + # The context manager ensures the connection is properly opened and closed + with User(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as u: + + # Call the method to retrieve all users from the system + # The result is returned as a JSON string + raw_result = u.getAllUsers() + + # Parse the JSON string into a Python object (list/dictionary) + data = json.loads(raw_result) + + # Pretty-print the parsed data with indentation for readability + print(json.dumps(data, indent=4)) + + # Handle any exceptions that occur during connection or data retrieval + except Exception as e: + # Print the error message for debugging + print(e) + + + +# ---------------------------------------------------- +# Get single Users information from IBM i Server +# using iLibrary +# ---------------------------------------------------- +def getSingleUser(): + # Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE = False + + # Username to search for in the IBM i system + USERNAME_TO_SEARCH = '' + + try: + # Establish a connection to the IBM i system using the User class + # The context manager ensures proper connection handling (open/close) + with User(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as u: + + # Retrieve detailed information for a single user + # - username: the user profile to look up + # Returns data as a JSON string + raw_result = u.getSingleUserInformation( + username=USERNAME_TO_SEARCH, + ) + + # Convert the JSON string into a Python object (dict or list) + data = json.loads(raw_result) + + # Pretty-print the result for better readability + print(json.dumps(data, indent=4)) + + # Handle any exceptions that occur during execution + except Exception as e: + # Output the error message for debugging purposes + print(e) + +# ---------------------------------------------------- +# Send a message to a IBM i User using iLibrary +# ---------------------------------------------------- +def sendMessage(): + # Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE = False + + # Username to search for in the IBM i system + USERNAME_TO_SEARCH = '' + MESSAGE_TO_SEND = 'From iLibrary' + + try: + # Establish a connection to the IBM i system using the User class + # The context manager ensures proper connection handling (open/close) + with User(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as u: + + # Retrieve detailed information for a single user + # - username: the user profile to look up + # Returns data as a JSON string + raw_result = u.send_message_to_user( + username=USERNAME_TO_SEARCH, + message=MESSAGE_TO_SEND, + ) + + # Convert the JSON string into a Python object (dict or list) + data = json.loads(raw_result) + + # Pretty-print the result for better readability + print(json.dumps(data, indent=4)) + + # Handle any exceptions that occur during execution + except Exception as e: + # Output the error message for debugging purposes + print(e) + + +if __name__ == "__main__": + getAllUsers() + getSingleUser() + sendMessage() +``` +**Integrated File System (IFS):** +```python +import json +from os.path import join +import os +from dotenv import load_dotenv +from iLibrary import IFS + +#load ENV file and get the Connection Settings +dotenv_path = join('.env') +load_dotenv(dotenv_path) +DB_DRIVER = os.environ.get("DB_DRIVER") +DB_USER = os.environ.get("DB_USER") +DB_PASSWORD = os.environ.get("DB_PASSWORD") +DB_SYSTEM = os.environ.get("DB_SYSTEM") + + +if __name__ == "__main__": + +# ---------------------------------------------------- +# Read the IFS with iLibrary +# ---------------------------------------------------- + + # Path in the IBM i Integrated File System (IFS) to read + REMOTE_PATH_TO_READ: str = '/home/' + + # If True, reads all subdirectories recursively + # If False, reads only the specified directory + SUBTREE: bool = False + + # Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE: bool = False + + try: + # Try to establish a connection to the IBM i server + # The IFS class is used as a context manager to ensure proper cleanup + with IFS(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as i: + + # Call the readIFS method to retrieve file system data + # - path_to_read: directory path in the IFS + # - subtrees: whether to include subdirectories + raw_result = i.readIFS( + path_to_read=REMOTE_PATH_TO_READ, + subtrees=SUBTREE + ) + + # Convert the returned JSON string into a Python object (dict/list) + data = json.loads(raw_result) + + # Pretty-print the JSON data with indentation for readability + print(json.dumps(data, indent=2)) + + # Catch and handle any errors that occur during execution + except Exception as e: + # Print a simple error message for debugging purposes + print(f"An error occurred: {e}") +``` \ No newline at end of file diff --git a/Examples/example.env b/Examples/example.env new file mode 100644 index 0000000..79c82f9 --- /dev/null +++ b/Examples/example.env @@ -0,0 +1,4 @@ +DB_DRIVER='' #recomment use the 'IBM i Access ODBC Driver' +DB_SYSTEM= +DB_PASSWORD= +DB_USER= \ No newline at end of file diff --git a/Examples/example_ifs.py b/Examples/example_ifs.py new file mode 100644 index 0000000..7053011 --- /dev/null +++ b/Examples/example_ifs.py @@ -0,0 +1,54 @@ +import json +from os.path import join +import os +from dotenv import load_dotenv +from iLibrary import IFS + +#load ENV file and get the Connection Settings +dotenv_path = join('.env') +load_dotenv(dotenv_path) +DB_DRIVER = os.environ.get("DB_DRIVER") +DB_USER = os.environ.get("DB_USER") +DB_PASSWORD = os.environ.get("DB_PASSWORD") +DB_SYSTEM = os.environ.get("DB_SYSTEM") + + +if __name__ == "__main__": + +# ---------------------------------------------------- +# Read the IFS with iLibrary +# ---------------------------------------------------- + + # Path in the IBM i Integrated File System (IFS) to read + REMOTE_PATH_TO_READ: str = '/home/' + + # If True, reads all subdirectories recursively + # If False, reads only the specified directory + SUBTREE: bool = False + + # Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE: bool = False + + try: + # Try to establish a connection to the IBM i server + # The IFS class is used as a context manager to ensure proper cleanup + with IFS(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as i: + + # Call the readIFS method to retrieve file system data + # - path_to_read: directory path in the IFS + # - subtrees: whether to include subdirectories + raw_result = i.readIFS( + path_to_read=REMOTE_PATH_TO_READ, + subtrees=SUBTREE + ) + + # Convert the returned JSON string into a Python object (dict/list) + data = json.loads(raw_result) + + # Pretty-print the JSON data with indentation for readability + print(json.dumps(data, indent=2)) + + # Catch and handle any errors that occur during execution + except Exception as e: + # Print a simple error message for debugging purposes + print(f"An error occurred: {e}") \ No newline at end of file diff --git a/Examples/example_library.py b/Examples/example_library.py new file mode 100644 index 0000000..8ff2374 --- /dev/null +++ b/Examples/example_library.py @@ -0,0 +1,124 @@ +import json +from os.path import join +import os +from dotenv import load_dotenv +from iLibrary import Library + +#load ENV file and get the Connection Settings +dotenv_path = join('.env') +load_dotenv(dotenv_path) +DB_DRIVER = os.environ.get("DB_DRIVER") +DB_USER = os.environ.get("DB_USER") +DB_PASSWORD = os.environ.get("DB_PASSWORD") +DB_SYSTEM = os.environ.get("DB_SYSTEM") + + + + +# ---------------------------------------------------- +# make a Savefile from a library +# ---------------------------------------------------- +def getSaveFile(): + + # Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE = False + + + PORT = 22 #for the Pub400.com use the Port 2222 + LIBRARY = '' + SAVE_FILE_NAME = 'FOO' + LOCAL_PATH = 'YOUR_LOCAL_PATH' + DESCRIPTION = 'Saved from iLibrary' + try: + # Establish a connection to the IBM i system using the User class + # The context manager ensures the connection is properly opened and closed + with Library(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as l: + + # Call the method to save the Library + # The result is returned as a JSON string + raw_result = l.saveLibrary( + library=LIBRARY, + saveFileName=SAVE_FILE_NAME, + description=DESCRIPTION, + localPath=LOCAL_PATH, + port=PORT + ) + + # Parse the JSON string into a Python object (list/dictionary) + data = json.loads(raw_result) + + # Pretty-print the parsed data with indentation for readability + print(json.dumps(data, indent=4)) + + # Handle any exceptions that occur during connection or data retrieval + except Exception as e: + # Print the error message for debugging + print(e) + + + +# ---------------------------------------------------- +# Get library information about all libraries from +# IBM i Server using iLibrary +# ---------------------------------------------------- +def getAllLibraries(): + # Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE = False + + try: + # Establish a connection to the IBM i system using the User class + # The context manager ensures the connection is properly opened and closed + with Library(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as l: + + # Call the method to get all libraries on the system + # The result is returned as a JSON string + raw_result = l.getAllLibraries() + + # Parse the JSON string into a Python object (list/dictionary) + data = json.loads(raw_result) + + # Pretty-print the parsed data with indentation for readability + print(json.dumps(data, indent=4)) + + # Handle any exceptions that occur during connection or data retrieval + except Exception as e: + # Print the error message for debugging + print(e) + + + + +# ---------------------------------------------------- +# Get single library information from IBM i Server +# using iLibrary +# ---------------------------------------------------- +def getSingleLibraryInfo(): + # Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE = False + LIBRARY = '' + try: + # Establish a connection to the IBM i system using the User class + # The context manager ensures the connection is properly opened and closed + with Library(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as l: + + # Call the method to show singe information about the Library + # The result is returned as a JSON string + raw_result = l.getLibraryInfo( + library=LIBRARY + ) + + # Parse the JSON string into a Python object (list/dictionary) + data = json.loads(raw_result) + + # Pretty-print the parsed data with indentation for readability + print(json.dumps(data, indent=4)) + + # Handle any exceptions that occur during connection or data retrieval + except Exception as e: + # Print the error message for debugging + print(e) + +if __name__ == '__main__': + getSaveFile() + getAllLibraries() + getSingleLibraryInfo() \ No newline at end of file diff --git a/Examples/example_user.py b/Examples/example_user.py new file mode 100644 index 0000000..a9012d7 --- /dev/null +++ b/Examples/example_user.py @@ -0,0 +1,119 @@ +import json +from os.path import join +import os +from dotenv import load_dotenv +from iLibrary import User + +#load ENV file and get the Connection Settings +dotenv_path = join('.env') +load_dotenv(dotenv_path) +DB_DRIVER = os.environ.get("DB_DRIVER") +DB_USER = os.environ.get("DB_USER") +DB_PASSWORD = os.environ.get("DB_PASSWORD") +DB_SYSTEM = os.environ.get("DB_SYSTEM") + +# ---------------------------------------------------- +# Get all Users information from IBM i Server using +# iLibrary +# ---------------------------------------------------- + +def getAllUsers(): +# Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE = False + + try: + # Establish a connection to the IBM i system using the User class + # The context manager ensures the connection is properly opened and closed + with User(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as u: + + # Call the method to retrieve all users from the system + # The result is returned as a JSON string + raw_result = u.getAllUsers() + + # Parse the JSON string into a Python object (list/dictionary) + data = json.loads(raw_result) + + # Pretty-print the parsed data with indentation for readability + print(json.dumps(data, indent=4)) + + # Handle any exceptions that occur during connection or data retrieval + except Exception as e: + # Print the error message for debugging + print(e) + + + +# ---------------------------------------------------- +# Get single Users information from IBM i Server +# using iLibrary +# ---------------------------------------------------- +def getSingleUser(): + # Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE = False + + # Username to search for in the IBM i system + USERNAME_TO_SEARCH = '' + + try: + # Establish a connection to the IBM i system using the User class + # The context manager ensures proper connection handling (open/close) + with User(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as u: + + # Retrieve detailed information for a single user + # - username: the user profile to look up + # Returns data as a JSON string + raw_result = u.getSingleUserInformation( + username=USERNAME_TO_SEARCH, + ) + + # Convert the JSON string into a Python object (dict or list) + data = json.loads(raw_result) + + # Pretty-print the result for better readability + print(json.dumps(data, indent=4)) + + # Handle any exceptions that occur during execution + except Exception as e: + # Output the error message for debugging purposes + print(e) + +# ---------------------------------------------------- +# Send a message to a IBM i User using iLibrary +# ---------------------------------------------------- +def sendMessage(): + # Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE = False + + # Username to search for in the IBM i system + USERNAME_TO_SEARCH = '' + MESSAGE_TO_SEND = 'From iLibrary' + + try: + # Establish a connection to the IBM i system using the User class + # The context manager ensures proper connection handling (open/close) + with User(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as u: + + # Retrieve detailed information for a single user + # - username: the user profile to look up + # Returns data as a JSON string + raw_result = u.send_message_to_user( + username=USERNAME_TO_SEARCH, + message=MESSAGE_TO_SEND, + ) + + # Convert the JSON string into a Python object (dict or list) + data = json.loads(raw_result) + + # Pretty-print the result for better readability + print(json.dumps(data, indent=4)) + + # Handle any exceptions that occur during execution + except Exception as e: + # Output the error message for debugging purposes + print(e) + + +if __name__ == "__main__": + getAllUsers() + getSingleUser() + sendMessage() \ No newline at end of file diff --git a/README.md b/README.md index c86645d..dcd0ca2 100644 --- a/README.md +++ b/README.md @@ -27,48 +27,136 @@ Installation Quickstart ```python -from os.path import join, dirname +import json +from os.path import join import os from dotenv import load_dotenv from iLibrary import Library -# Load ENV file and get the connection settings -dotenv_path = join(dirname(__file__), '.env') +#load ENV file and get the Connection Settings +dotenv_path = join('.env') load_dotenv(dotenv_path) DB_DRIVER = os.environ.get("DB_DRIVER") DB_USER = os.environ.get("DB_USER") DB_PASSWORD = os.environ.get("DB_PASSWORD") DB_SYSTEM = os.environ.get("DB_SYSTEM") -with Library(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER) as lib: - print(lib.getLibraryInfo('QGPL', wantJson=True)) - print(lib.getFileInfo('QGPL')) -``` -Save a library to SAVF and optionally download it -```python -from os.path import dirname - -with Library(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER) as lib: - ok = lib.saveLibrary( - library='MYLIB', - saveFileName='MYLIBSAVE', - description='Backup', - localPath=dirname(__file__), - remPath='/home/MYUSER/', - getZip=True - ) - print('Saved:', ok) + + +# ---------------------------------------------------- +# make a Savefile from a library +# ---------------------------------------------------- +def getSaveFile(): + + # Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE = False + + + PORT = 22 #for the Pub400.com use the Port 2222 + LIBRARY = '' + SAVE_FILE_NAME = 'FOO' + LOCAL_PATH = 'YOUR_LOCAL_PATH' + DESCRIPTION = 'Saved from iLibrary' + try: + # Establish a connection to the IBM i system using the User class + # The context manager ensures the connection is properly opened and closed + with Library(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as l: + + # Call the method to save the Library + # The result is returned as a JSON string + raw_result = l.saveLibrary( + library=LIBRARY, + saveFileName=SAVE_FILE_NAME, + description=DESCRIPTION, + localPath=LOCAL_PATH, + port=PORT + ) + + # Parse the JSON string into a Python object (list/dictionary) + data = json.loads(raw_result) + + # Pretty-print the parsed data with indentation for readability + print(json.dumps(data, indent=4)) + + # Handle any exceptions that occur during connection or data retrieval + except Exception as e: + # Print the error message for debugging + print(e) + + + +# ---------------------------------------------------- +# Get library information about all libraries from +# IBM i Server using iLibrary +# ---------------------------------------------------- +def getAllLibraries(): + # Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE = False + + try: + # Establish a connection to the IBM i system using the User class + # The context manager ensures the connection is properly opened and closed + with Library(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as l: + + # Call the method to get all libraries on the system + # The result is returned as a JSON string + raw_result = l.getAllLibraries() + + # Parse the JSON string into a Python object (list/dictionary) + data = json.loads(raw_result) + + # Pretty-print the parsed data with indentation for readability + print(json.dumps(data, indent=4)) + + # Handle any exceptions that occur during connection or data retrieval + except Exception as e: + # Print the error message for debugging + print(e) + + + + +# ---------------------------------------------------- +# Get single library information from IBM i Server +# using iLibrary +# ---------------------------------------------------- +def getSingleLibraryInfo(): + # Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE = False + LIBRARY = '' + try: + # Establish a connection to the IBM i system using the User class + # The context manager ensures the connection is properly opened and closed + with Library(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as l: + + # Call the method to show singe information about the Library + # The result is returned as a JSON string + raw_result = l.getLibraryInfo( + library=LIBRARY + ) + + # Parse the JSON string into a Python object (list/dictionary) + data = json.loads(raw_result) + + # Pretty-print the parsed data with indentation for readability + print(json.dumps(data, indent=4)) + + # Handle any exceptions that occur during connection or data retrieval + except Exception as e: + # Print the error message for debugging + print(e) + +if __name__ == '__main__': + getSaveFile() + getAllLibraries() + getSingleLibraryInfo() ``` -API overview -- Library(db_user, db_password, db_host, db_driver) - - __enter__ / __exit__ for connection lifecycle - - iclose(): manually close the connection -- getLibraryInfo(library: str, wantJson: bool = True) -> str | tuple -- getFileInfo(library: str, qFiles: bool = False) -> str -- saveLibrary(library: str, saveFileName: str, ..., getZip: bool = False, ...) -> bool -- removeFile(library: str, saveFileName: str) -> bool + +More Examples: +- [Go to the Folder Examples](/Examples/Readme.md) + Configuration - Environment variables expected: @@ -77,12 +165,6 @@ Configuration - DB_PASSWORD - DB_SYSTEM -Documentation -- Installation guide: [[docs/installation.md]] -- Usage examples: [[docs/usage.md]] -- Full API reference: [[docs/api.md]] -- Troubleshooting: [[docs/troubleshooting.md]] - Contributing - Please run tests or the sample script before submitting changes. diff --git a/app/iLibrary/__init__.py b/app/iLibrary/__init__.py deleted file mode 100644 index ed34b76..0000000 --- a/app/iLibrary/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .src.Library import ( - Library, -) -from .src.User import( - User, -) \ No newline at end of file diff --git a/app/iLibrary/src/getInfoForLibrary.py b/app/iLibrary/src/getInfoForLibrary.py deleted file mode 100644 index d3fee13..0000000 --- a/app/iLibrary/src/getInfoForLibrary.py +++ /dev/null @@ -1,94 +0,0 @@ -import json -from datetime import datetime, date -from decimal import Decimal - - -class getInfoForLibrary: - def __init__(self, connection): - self.conn = connection - - def _convert_to_json_ready(self, row, description): - """Interne Hilfsmethode zur Typ-Konvertierung und Bereinigung.""" - row_dict = {} - titles = [col[0] for col in description] - - for i, value in enumerate(row): - key = titles[i] - # Typ-Prüfung für JSON-Serialisierung - if isinstance(value, (datetime, date)): - row_dict[key] = value.isoformat() - elif isinstance(value, Decimal): - row_dict[key] = float(value) - elif isinstance(value, bytes): - row_dict[key] = value.decode('utf-8', errors='replace') - elif value is None: - row_dict[key] = None - else: - # Entfernt unnötige Leerzeichen von CHAR-Feldern - row_dict[key] = str(value).strip() - return row_dict - - def getLibraryInfo(self, library: str, wantJson=True): - if not library or len(library) > 10: - raise ValueError("Ungültiger Bibliotheksname (max. 10 Zeichen).") - - sql_query = f"SELECT * FROM TABLE(QSYS2.LIBRARY_INFO(upper('{library}')))" - try: - with self.conn.cursor() as cursor: - cursor.execute(sql_query) - row = cursor.fetchone() - - if not row: - error_msg = {"error": f"No data found for library: {library}"} - return json.dumps(error_msg, indent=4) if wantJson else ("error", error_msg["error"]) - - if wantJson: - return json.dumps(self._convert_to_json_ready(row, cursor.description), indent=4) - - return row - except Exception as e: - print(f"Fehler bei getLibraryInfo: {e}") - return None - - def getFileInfo(self, library: str, qFiles: bool = False) -> str: - if not library: - return json.dumps([{"error": "A library name is required."}]) - - if qFiles: - sql = f"SELECT * FROM QSYS2.SYSMEMBERSTAT WHERE SYSTEM_TABLE_SCHEMA = '{library.upper()}' AND SOURCE_TYPE IS NOT NULL ORDER BY SYSTEM_TABLE_MEMBER" - else: - sql = f"SELECT * FROM TABLE(QSYS2.OBJECT_STATISTICS('{library.upper()}', '*ALL')) AS X" - - try: - with self.conn.cursor() as cursor: - cursor.execute(sql) - rows = cursor.fetchall() - - if not rows: - return json.dumps([{"error": f"No Files Found in Library: {library}"}]) - - result_list = [self._convert_to_json_ready(row, cursor.description) for row in rows] - self.conn.commit() - return json.dumps(result_list, indent=4) - except Exception as e: - if self.conn: self.conn.rollback() - return json.dumps([{"error": f"Database Error: {str(e)}"}]) - - def getAllLibraries(self): - # Hier nutzen wir nun auch die dynamische Spaltenerkennung statt der harten Liste - sql = "SELECT * FROM TABLE(QSYS2.OBJECT_STATISTICS('*ALL', '*LIB')) AS X" - try: - with self.conn.cursor() as cursor: - cursor.execute(sql) - rows = cursor.fetchall() - - if not rows: - return json.dumps([{"error": "No Libraries found"}]) - - result_list = [self._convert_to_json_ready(row, cursor.description) for row in rows] - self.conn.commit() - return json.dumps(result_list, indent=4) - except Exception as e: - print(f"Fehler bei getAllLibraries: {e}") - if self.conn: self.conn.rollback() - return False \ No newline at end of file diff --git a/app/iLibrary/src/iLibrary/IFS.py b/app/iLibrary/src/iLibrary/IFS.py new file mode 100644 index 0000000..eded7ea --- /dev/null +++ b/app/iLibrary/src/iLibrary/IFS.py @@ -0,0 +1,94 @@ +from mapepire_python import connect + +from .ifs.ifs_logic import * + +class IFS(getIFSClass): + """ + A class to manage User on IBMi System + + It provides methods to connect to the system via pyodbc for SQL and + paramiko for SFTP transfers. + """ + + # ------------------------------------------------------ + # __init__ - initzialise the class + # ------------------------------------------------------ + def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str, mapepire: bool = False): + """ + Initializes the class attributes for a database connection. + The actual connection is established in the __enter__ method. + + Args: + db_user (str): The user ID for the database connection. + db_password (str): The password for the database user. + db_host (str): The system/host name for the database connection. + db_driver (str): The ODBC driver to be used. + """ + self.db_user = db_user + self.db_host = db_host + self.db_driver = db_driver + self.db_password = db_password + self.mapepire = mapepire + + # ------------------------------------------------------ + # __enter__ - enter to the class + # ------------------------------------------------------ + def __enter__(self) -> 'IFS': + """ + Establishes the database connection when entering a 'with' block. + """ + try: + if not self.mapepire: + conn_str = ( + f"DRIVER={self.db_driver};" + f"SYSTEM={self.db_host};" + f"UID={self.db_user};" + f"PWD={self.db_password};" + ) + self.conn = pyodbc.connect(conn_str, autocommit=True) + + else: + conn_str = { + "host": self.db_host, + "port": 8076, + "user": self.db_user, + "password": self.db_password, + } + self.conn = connect(conn_str) + super().__init__(self.conn, mapepire=self.mapepire) + return self + + except pyodbc.Error as ex: + sqlstate = ex.args[0] + print(f"Database connection failed with error: {sqlstate}") + raise + except Exception as e: + + print(f"Database connection failed with error: {e}") + raise + + # ------------------------------------------------------ + # __exit__ - leave the class + # ------------------------------------------------------ + def __exit__(self, exc_type, exc_val, exc_tb): + """ + Closes the database connection when exiting a 'with' block. + This method is called automatically, even if an error occurred. + """ + self.iclose() + + # ------------------------------------------------------ + # iClose - close connection + # ------------------------------------------------------ + def iclose(self): + if not self.conn: + return + + try: + # Both pyodbc and mapepire-python support .close() + # but mapepire MUST have it called to kill background threads + self.conn.close() + except Exception: + pass + finally: + self.conn = None \ No newline at end of file diff --git a/app/iLibrary/src/__init__.py b/app/iLibrary/src/iLibrary/Libr/__init__.py similarity index 100% rename from app/iLibrary/src/__init__.py rename to app/iLibrary/src/iLibrary/Libr/__init__.py diff --git a/app/iLibrary/src/iLibrary/Libr/getInfoForLibrary.py b/app/iLibrary/src/iLibrary/Libr/getInfoForLibrary.py new file mode 100644 index 0000000..2d6a09a --- /dev/null +++ b/app/iLibrary/src/iLibrary/Libr/getInfoForLibrary.py @@ -0,0 +1,155 @@ +from ..util_functions.helper import create_success_envelope, create_error_envelope + + + +class getInfoForLibrary: + def __init__(self, connection, mapepire=False): + self.conn = connection + self.mapepire = mapepire + + + def getLibraryInfo(self, library: str): + if not library or len(library) > 10: + # This will be caught by the 'except' block below + return create_error_envelope('Invalid library name (max. 10 characters).', 'getLibraryInfo') + + + sql_query = f"SELECT * FROM TABLE(QSYS2.LIBRARY_INFO(upper('{library}')))" + + try: + with self.conn.cursor() as cursor: + cursor.execute(sql_query) + + # Handle Mapepire special case + if self.mapepire: + result_raw = cursor.fetchall() + # If Mapepire returns a dict with a 'data' key: + data = result_raw.get('data', []) if isinstance(result_raw, dict) else result_raw + return create_success_envelope(data) + + # Standard Cursor Handling + rows = cursor.fetchall() + + if not rows: + # Return 200 but with empty data - this is standard for "No results" + return create_success_envelope([], message="No data found") + + # Get column names and format results + columns = [column[0] for column in cursor.description] + results = [dict(zip(columns, r)) for r in rows] + + return create_success_envelope(results) + + except Exception as e: + return create_error_envelope(str(e), 'getLibraryInfo') + + + + def getFileInfo(self, library: str, qFiles: bool = False) -> str: + if not library: + return create_error_envelope('Required a library name', 'getFileInfo') + + if qFiles: + sql = f"SELECT * FROM QSYS2.SYSMEMBERSTAT WHERE SYSTEM_TABLE_SCHEMA = '{library.upper()}' AND SOURCE_TYPE IS NOT NULL ORDER BY SYSTEM_TABLE_MEMBER" + else: + sql = f"SELECT * FROM TABLE(QSYS2.OBJECT_STATISTICS('{library.upper()}', '*ALL')) AS X" + + try: + with self.conn.cursor() as cursor: + cursor.execute(sql) + + # Mapepire often returns a dict with a 'data' key; standard DB-API returns a list of tuples. + rows = cursor.fetchall() + + # 1. Standardize data extraction + data_payload = rows['data'] if isinstance(rows, dict) else rows + + # 2. Check if data is None or empty + if not data_payload: + return create_error_envelope(f'No Files Found in Library: {library}', 'getFileInfo') + + # 3. Handle Mapepire direct return + if self.mapepire: + return create_success_envelope(data_payload) + + # 4. Process standard DB-API results (List of Tuples -> List of Dicts) + columns = [column[0] for column in cursor.description] + results = [dict(zip(columns, r)) for r in data_payload] + + # Explicit commit to release any read-locks depending on isolation level + self.conn.commit() + return create_success_envelope(results) + + except Exception as e: + # Safely rollback if the connection exists and supports it + if hasattr(self, 'conn') and self.conn: + self.conn.rollback() + return create_error_envelope(str(e), 'getFileInfo') + + def getAllLibraries(self): + sql = "SELECT * FROM TABLE(QSYS2.OBJECT_STATISTICS('*ALL', '*LIB')) AS X" + try: + with self.conn.cursor() as cursor: + cursor.execute(sql) + + # Mapepire often returns a dict with a 'data' key; standard DB-API returns a list of tuples. + rows = cursor.fetchall() + + # Standardize data extraction + data_payload = rows['data'] if isinstance(rows, dict) else rows + + # Check if data is None or empty + if not data_payload: + return create_error_envelope(f'NoLibrary found', 'getAllLibraries') + + # Handle Mapepire direct return + if self.mapepire: + return create_success_envelope(data_payload) + + # Process standard DB-API results (List of Tuples -> List of Dicts) + columns = [column[0] for column in cursor.description] + results = [dict(zip(columns, r)) for r in data_payload] + + # Explicit commit to release any read-locks depending on isolation level + self.conn.commit() + return create_success_envelope(results) + + except Exception as e: + # Safely rollback if the connection exists and supports it + if hasattr(self, 'conn') and self.conn: + self.conn.rollback() + return create_error_envelope(str(e), 'getAllLibraries') + + + + + # #-------------------------------------------- + # # Helper Functions to get better Error Mails + # # -------------------------------------------- + # def __create_success_envelope(self, data, message="successful"): + # timestamp = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z') + # return json.dumps({ + # "success": True, + # "code": 200, + # "message": message, + # "metadata": { + # "timestamp": timestamp, + # "count": len(data) + # }, + # "data": data, + # "error": None + # }, indent=4, default=str) + # + # def __create_error_envelope(self, error_msg:str, func_name:str): + # timestamp = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z') + # return json.dumps({ + # "success": False, + # "code": 500, + # "message": f"An error occurred in {func_name}", + # "metadata": { + # "timestamp": timestamp, + # "count": 0 + # }, + # "data": [], + # "error": {"details": error_msg} + # }, indent=4, default=str) \ No newline at end of file diff --git a/app/iLibrary/src/iLibrary/Libr/saveLibrary.py b/app/iLibrary/src/iLibrary/Libr/saveLibrary.py new file mode 100644 index 0000000..d5a5a91 --- /dev/null +++ b/app/iLibrary/src/iLibrary/Libr/saveLibrary.py @@ -0,0 +1,198 @@ +import paramiko +from typing import Union +from pathlib import PureWindowsPath, Path +from ..util_functions.helper import create_success_envelope, create_error_envelope + + +class saveLibrary: + def __init__(self, connection, mapepire=False): + """ + Initializes the saveLibrary parent class. + """ + self.conn = connection + self.mapepire = mapepire + + def saveLibrary(self, + library: str, + saveFileName: str, + dev: str = None, + vol: str = None, + toLibrary: str = None, + description: str = None, + localPath: str = None, + remPath: str = None, + getZip: bool = True, + port: int = None, + version: str = None, + max_records: Union[int, str, None] = None, + asp: Union[int, str, None] = None, + waitFile: Union[int, str, None] = None, + share: str = None, + authority: str = None + ) -> dict[str, str]: + + trgList = ["V1R1M0", "V1R1M2", "V1R2M0", "V1R3M0", "V2R1M0", "V2R1M1", + "V2R2M0", "V2R3M0", "V3R0M5", "V3R1M0", "V3R2M0", "V3R6M0", + "V3R7M0", "V4R1M0", "V4R2M0", "V4R3M0", "V4R4M0", "V4R5M0", + "V5R1M0", "V5R2M0", "V5R3M0", "V5R4M0", "V6R1M0", "V6R1M1", + "V7R1M0", "V7R2M0", "V7R3M0", "V7R4M0", "V7R5M0", "V7R6M0"] + + if not library: + return create_error_envelope(error_msg="Library not found.", func_name='saveLibrary') + if not saveFileName: + return create_error_envelope(error_msg="A save file name is required.", func_name='saveLibrary') + with self.conn.cursor() as cursor: + cursor.execute("SELECT COUNT(*) as counter FROM TABLE(QSYS2.LIBRARY_INFO(?))", (library.upper(),)) + data = cursor.fetchone() + if self.mapepire: + counter = data['data'][0].get('COUNTER') + else: + counter = data[0] + if counter != 1: + return create_error_envelope(error_msg="Library not found.", func_name='saveLibrary') + # Standardize inputs + + toLibrary = toLibrary if toLibrary else library + version = version.upper() if version and version.upper() in trgList else "*CURRENT" + + + # Build SAVLIB command + command_str = f'SAVLIB LIB({library.upper().strip()})' + command_str += f' DEV({dev.upper() if dev in ["*SAVF", "*MEDDFN"] else "*SAVF"})' + if vol == '*MOUNTED': + command_str += f' VOL({vol})' + + try: + # 1. Create the SAVF on IBM i + if self.__crtsavf(saveFileName, toLibrary, description, max_records, asp, waitFile, share, authority): + command_str += f" SAVF({toLibrary.strip()}/{saveFileName.strip()}) TGTRLS({version.strip()})" + + with self.conn.cursor() as cursor: + # Execute the Save Library command + cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str,)) + + # 2. Handle SFTP Download if requested + if getZip: + # Resolve local path logic + local_dir = Path(localPath) if localPath else Path.home() / "Downloads" + local_dir.mkdir(parents=True, exist_ok=True) + destination_local_path = str(local_dir / f"{saveFileName.upper()}.savf") + + # Normalize remote ifs path (Ensure remPath is provided if getZip is True) + rem_dir = remPath.rstrip('/') if remPath else '/tmp' + remote_temp_savf_path = f"{rem_dir}/{saveFileName.upper()}.savf" + + # Copy from Library (*FILE) to ifs (*STMF) + copy_cmd = ( + f"CPYTOSTMF FROMMBR('/QSYS.LIB/{toLibrary.upper().strip()}.LIB/{saveFileName.upper().strip()}.FILE') " + f"TOSTMF('{remote_temp_savf_path}') STMFOPT(*REPLACE)" + ) + cursor.execute("CALL QSYS2.QCMDEXC(?)", (copy_cmd,)) + + # Perform SFTP transfer + if self.__getSavFile(localFilePath=destination_local_path, remotePath=remote_temp_savf_path, + port=port): + # Clean up the temporary ifs file + rmv_ifs_cmd = f"QSH CMD('rm -f {remote_temp_savf_path}')" + cursor.execute("CALL QSYS2.QCMDEXC(?)", (rmv_ifs_cmd,)) + success_msg = f"Success: Downloaded to {destination_local_path}" + else: + return create_error_envelope(error_msg="Error: SFTP transfer failed.", + func_name='saveLibrary') + + + if not self.mapepire: + self.conn.commit() + + return create_success_envelope(data=[], message=success_msg) + + else: + return create_error_envelope(error_msg="Failed to create Save File (SAVF).", func_name='saveLibrary') + + except Exception as e: + if not self.mapepire: + self.conn.rollback() + return create_error_envelope(error_msg=str(e), func_name='saveLibrary') + finally: + self.removeFile(library=toLibrary, saveFileName=saveFileName) + + + def __crtsavf(self, saveFileName, library, description, max_records, asp, waitFile, share, authority) -> bool: + if not description: description = 'A SaveFile from iLibrary' + cmd = f"CRTSAVF FILE({library.upper().strip()}/{saveFileName.upper().strip()}) TEXT('{description.strip()}')" + + # Validation + val_max = self.__validate_max_value(max_records, 'max_records', ['*NOMAX'], max_limit=4293525600) + if val_max: cmd += f" MAXRCDS({val_max})" + val_asp = self.__validate_max_value(asp, 'asp', ['*LIBASP'], max_limit=32) + if val_asp: cmd += f" ASP({val_asp})" + if authority: cmd += f" AUT({authority.upper()})" + + try: + with self.conn.cursor() as cursor: + cursor.execute("CALL QSYS2.QCMDEXC(?)", (cmd,)) + if not self.mapepire: self.conn.commit() + return True + except Exception as e: + if "already exists" in str(e).lower() or (len(e.args) > 0 and e.args[0] == 'HY000'): + self.removeFile(library, saveFileName) + try: + with self.conn.cursor() as cursor: + cursor.execute("CALL QSYS2.QCMDEXC(?)", (cmd,)) + if not self.mapepire: self.conn.commit() + return True + except: + return False + return False + + def __getSavFile(self, localFilePath: str, remotePath: str, port: int = None) -> bool: + connect_port = port if port else 22 + ssh_client = paramiko.SSHClient() + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + + try: + ssh_client.connect( + hostname=self.db_host, + username=self.db_user, + password=self.db_password, + port=connect_port, + timeout=15 + ) + + with ssh_client.open_sftp() as ftp_client: + rem_path_posix = PureWindowsPath(remotePath).as_posix() + + # Check if remote file exists before trying to get it + try: + ftp_client.stat(rem_path_posix) + except FileNotFoundError: + return False + + ftp_client.get(rem_path_posix, localFilePath) + return True + + except Exception as e: + return False + finally: + ssh_client.close() + + def removeFile(self, library: str, saveFileName: str) -> bool: + cmd = f"DLTF FILE({library.upper().strip()}/{saveFileName.upper().strip()})" + try: + with self.conn.cursor() as cursor: + cursor.execute("CALL QSYS2.QCMDEXC(?)", (cmd,)) + if not self.mapepire: self.conn.commit() + return True + except: + return False + + def __validate_max_value(self, value, param_name, str_format, min_limit=1, max_limit=None): + if value is None: return False + if isinstance(value, str) and value.upper() in str_format: return value.upper() + try: + num = int(value) + if max_limit and (num < min_limit or num > max_limit): return False + return num + except: + return False diff --git a/app/iLibrary/src/Library.py b/app/iLibrary/src/iLibrary/Library.py similarity index 62% rename from app/iLibrary/src/Library.py rename to app/iLibrary/src/iLibrary/Library.py index ca1665b..09e886f 100644 --- a/app/iLibrary/src/Library.py +++ b/app/iLibrary/src/iLibrary/Library.py @@ -1,11 +1,7 @@ -from os.path import join -import paramiko +from mapepire_python import connect import pyodbc -import json -from datetime import datetime, date -from decimal import Decimal -from .getInfoForLibrary import * -from .saveLibrary import * +from .Libr.getInfoForLibrary import * +from .Libr.saveLibrary import * @@ -20,7 +16,7 @@ class Library(getInfoForLibrary, saveLibrary): # ------------------------------------------------------ # __init__ - initzialise the class # ------------------------------------------------------ - def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str): + def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str, mapepire: bool = False): """ Initializes the class attributes for a database connection. The actual connection is established in the __enter__ method. @@ -35,6 +31,7 @@ def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str) self.db_host = db_host self.db_driver = db_driver self.db_password = db_password + self.mapepire = mapepire # ------------------------------------------------------ # __enter__ - enter to the class @@ -44,18 +41,34 @@ def __enter__(self) -> 'Library': Establishes the database connection when entering a 'with' block. """ try: - conn_str = ( - f"DRIVER={self.db_driver};" - f"SYSTEM={self.db_host};" - f"UID={self.db_user};" - f"PWD={self.db_password};" - ) - self.conn = pyodbc.connect(conn_str, autocommit=True) + if not self.mapepire: + conn_str = ( + f"DRIVER={self.db_driver};" + f"SYSTEM={self.db_host};" + f"UID={self.db_user};" + f"PWD={self.db_password};" + ) + self.conn = pyodbc.connect(conn_str, autocommit=True) + + else: + conn_str = { + "host": self.db_host, + "port": 8076, + "user": self.db_user, + "password": self.db_password, + } + self.conn = connect(conn_str) + super().__init__(self.conn, mapepire=self.mapepire) return self + except pyodbc.Error as ex: sqlstate = ex.args[0] print(f"Database connection failed with error: {sqlstate}") raise + except Exception as e: + + print(f"Database connection failed with error: {e}") + raise # ------------------------------------------------------ # __exit__ - leave the class @@ -72,9 +85,14 @@ def __exit__(self, exc_type, exc_val, exc_tb): # iClose - close connection # ------------------------------------------------------ def iclose(self): - """ - A helper method to close the connection, also useful for manual closure. - """ - if self.conn and not self.conn.closed: + if not self.conn: + return + + try: + # Both pyodbc and mapepire-python support .close() + # but mapepire MUST have it called to kill background threads self.conn.close() - pass \ No newline at end of file + except Exception: + pass + finally: + self.conn = None \ No newline at end of file diff --git a/app/iLibrary/src/User.py b/app/iLibrary/src/iLibrary/User.py similarity index 62% rename from app/iLibrary/src/User.py rename to app/iLibrary/src/iLibrary/User.py index e7cd26b..456328c 100644 --- a/app/iLibrary/src/User.py +++ b/app/iLibrary/src/iLibrary/User.py @@ -1,11 +1,7 @@ -from os.path import join -import paramiko -import pyodbc -import json -from datetime import datetime, date -from decimal import Decimal -from .getUserInfoForUser import * -from .sendMSG import * +from mapepire_python import connect + +from .Usr.getUserInfoForUser import * +from .Usr.sendMSG import * class User(getUserInfoForUser, sendMSG): """ @@ -18,7 +14,7 @@ class User(getUserInfoForUser, sendMSG): # ------------------------------------------------------ # __init__ - initzialise the class # ------------------------------------------------------ - def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str): + def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str, mapepire: bool = False): """ Initializes the class attributes for a database connection. The actual connection is established in the __enter__ method. @@ -33,6 +29,7 @@ def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str) self.db_host = db_host self.db_driver = db_driver self.db_password = db_password + self.mapepire = mapepire # ------------------------------------------------------ # __enter__ - enter to the class @@ -42,18 +39,34 @@ def __enter__(self) -> 'User': Establishes the database connection when entering a 'with' block. """ try: - conn_str = ( - f"DRIVER={self.db_driver};" - f"SYSTEM={self.db_host};" - f"UID={self.db_user};" - f"PWD={self.db_password};" - ) - self.conn = pyodbc.connect(conn_str, autocommit=True) + if not self.mapepire: + conn_str = ( + f"DRIVER={self.db_driver};" + f"SYSTEM={self.db_host};" + f"UID={self.db_user};" + f"PWD={self.db_password};" + ) + self.conn = pyodbc.connect(conn_str, autocommit=True) + + else: + conn_str = { + "host": self.db_host, + "port": 8076, + "user": self.db_user, + "password": self.db_password, + } + self.conn = connect(conn_str) + super().__init__(self.conn, mapepire=self.mapepire) return self + except pyodbc.Error as ex: sqlstate = ex.args[0] print(f"Database connection failed with error: {sqlstate}") raise + except Exception as e: + + print(f"Database connection failed with error: {e}") + raise # ------------------------------------------------------ # __exit__ - leave the class @@ -69,9 +82,14 @@ def __exit__(self, exc_type, exc_val, exc_tb): # iClose - close connection # ------------------------------------------------------ def iclose(self): - """ - A helper method to close the connection, also useful for manual closure. - """ - if self.conn and not self.conn.closed: + if not self.conn: + return + + try: + # Both pyodbc and mapepire-python support .close() + # but mapepire MUST have it called to kill background threads self.conn.close() - pass \ No newline at end of file + except Exception: + pass + finally: + self.conn = None \ No newline at end of file diff --git a/app/iLibrary/src/iLibrary/Usr/__init__.py b/app/iLibrary/src/iLibrary/Usr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/iLibrary/src/getUserInfoForUser.py b/app/iLibrary/src/iLibrary/Usr/getUserInfoForUser.py similarity index 68% rename from app/iLibrary/src/getUserInfoForUser.py rename to app/iLibrary/src/iLibrary/Usr/getUserInfoForUser.py index 47a46a9..2958758 100644 --- a/app/iLibrary/src/getUserInfoForUser.py +++ b/app/iLibrary/src/iLibrary/Usr/getUserInfoForUser.py @@ -1,11 +1,10 @@ -from os.path import join -import paramiko import pyodbc -import json -from datetime import datetime, date -from decimal import Decimal +from ..util_functions.helper import create_success_envelope, create_error_envelope class getUserInfoForUser(): + def __init__(self, connection, mapepire=False): + self.conn = connection + self.mapepire = mapepire """ Handles user information retrieval and messaging functionalities. @@ -16,7 +15,7 @@ class getUserInfoForUser(): :ivar conn: Database connection object used for executing queries. :type conn: Any """ - def getAllUsers(self, wantJson: bool = False): + def getAllUsers(self) -> dict[str, str]: """ Retrieves all user information from the database. Optionally returns the data in JSON format depending on the provided parameter. @@ -33,35 +32,31 @@ def getAllUsers(self, wantJson: bool = False): """ sql_query = "SELECT * FROM qsys2.user_info" - def json_serial(obj): - if hasattr(obj, 'isoformat'): - return obj.isoformat() - return str(obj) try: with self.conn.cursor() as cursor: cursor.execute(sql_query) rows = cursor.fetchall() - + if self.mapepire: + data = rows.get('data', []) if isinstance(rows, dict) else rows + return create_success_envelope(data) if not rows: - error_msg = {'error': 'No data found'} - return json.dumps(error_msg, indent=4) if wantJson else [("error", "No data found")] + error_msg = f"No User found" + return create_error_envelope(error_msg, func_name="getAllUsers") # Get column names columns = [column[0] for column in cursor.description] - if wantJson: - # Create a LIST of dictionaries - results = [dict(zip(columns, r)) for r in rows] - return json.dumps(results, indent=4, default=json_serial) - return rows # Returns the list of tuples + results = [dict(zip(columns, r)) for r in rows] + return create_success_envelope(results) + + except Exception as e: - print(f"An error occurred: {e}") - return None + return create_error_envelope(error_msg=str(e), func_name="getAllUsers") - def getSingleUserInformation(self, username: str, wantJson: bool = False): + def getSingleUserInformation(self, username: str) -> dict[str, str]: """ Retrieves information about a specific user from the database based on their username. The function supports returning data either as a JSON-formatted string or as a tuple with corresponding database fields. @@ -90,20 +85,23 @@ def json_serial(obj): try: with self.conn.cursor() as cursor: cursor.execute(sql_query) - row = cursor.fetchone() # Since you only expect one user - - if not row: - error_msg = {'error': 'No data found for User: ' + username} - return json.dumps(error_msg, indent=4) if wantJson else ("error", error_msg['error']) + row = cursor.fetchone() + if self.mapepire: + #check if row empty or not + if not row['data']: + error_msg = 'No data found for User: ' + username + return create_error_envelope(error_msg, func_name="getSingleUserInformation") + return create_success_envelope(row['data']) + if not row : + error_msg = 'No data found for User: ' + username + return create_error_envelope(error_msg, func_name="getSingleUserInformation") # DYNAMICALLY get column names from the database itself columns = [column[0] for column in cursor.description] row_dict = dict(zip(columns, row)) - if wantJson: - return json.dumps(row_dict, indent=4, default=json_serial) - return row # Returns the tuple + return create_success_envelope(row_dict) except Exception as e: print(f"An error occurred: {e}") - return None + return create_error_envelope(error_msg=str(e), func_name="getSingleUserInformation") diff --git a/app/iLibrary/src/sendMSG.py b/app/iLibrary/src/iLibrary/Usr/sendMSG.py similarity index 72% rename from app/iLibrary/src/sendMSG.py rename to app/iLibrary/src/iLibrary/Usr/sendMSG.py index 3a7c11d..bf55ad7 100644 --- a/app/iLibrary/src/sendMSG.py +++ b/app/iLibrary/src/iLibrary/Usr/sendMSG.py @@ -1,9 +1,5 @@ -from os.path import join -import paramiko -import pyodbc -import json -from datetime import datetime, date -from decimal import Decimal +from ..util_functions.helper import create_success_envelope, create_error_envelope + class sendMSG(): """ @@ -15,6 +11,9 @@ class sendMSG(): Attributes supported by this class are not specified because the class relies on method-level operations. """ + def __init__(self, connection, mapepire=False): + self.conn = connection + self.mapepire = mapepire def send_message_to_user( self, username: str, @@ -23,7 +22,7 @@ def send_message_to_user( # msgtype: str = None, # rpymsgq: str = None, ccsid: int = None - ): + ) -> dict[str, str]: """ Sends a message to a specified user on the system. This method interacts with the system to send a message by executing an SQL query. It validates the required @@ -53,6 +52,15 @@ def send_message_to_user( username = username.upper() message = message.upper() + with self.conn.cursor() as cursor: + cursor.execute("SELECT COUNT(*) as counter FROM QSYS2.USER_INFO WHERE AUTHORIZATION_NAME = ?", (username,)) + data = cursor.fetchone() + if self.mapepire: + counter = data['data'][0].get('COUNTER') + else: + counter = data[0] + if counter != 1: + return create_error_envelope(error_msg="User not Found", func_name='sendMSG') sql_query = f"CALL QSYS2.QCMDEXC('SNDMSG MSG(''{message}'') TOUSR({username})')" if ccsid: @@ -60,8 +68,7 @@ def send_message_to_user( try: with self.conn.cursor() as cursor: cursor.execute(sql_query) - row_dict:dict = {"success": f'Message sent to {username}'} - return json.dumps(row_dict, indent=4) + row_dict= f'Message sent to {username}' + return create_success_envelope(row_dict) except Exception as e: - row_dict: dict = {"error" : f'Error: {e}'} - return json.dumps(row_dict, indent=4) \ No newline at end of file + return create_error_envelope(str(e), 'sendMSG') \ No newline at end of file diff --git a/app/iLibrary/src/iLibrary/__init__.py b/app/iLibrary/src/iLibrary/__init__.py new file mode 100644 index 0000000..7907946 --- /dev/null +++ b/app/iLibrary/src/iLibrary/__init__.py @@ -0,0 +1,3 @@ +from .Library import Library +from .User import User +from .IFS import IFS \ No newline at end of file diff --git a/app/iLibrary/src/iLibrary/ifs/__init__.py b/app/iLibrary/src/iLibrary/ifs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/iLibrary/src/iLibrary/ifs/ifs_logic.py b/app/iLibrary/src/iLibrary/ifs/ifs_logic.py new file mode 100644 index 0000000..e7cad38 --- /dev/null +++ b/app/iLibrary/src/iLibrary/ifs/ifs_logic.py @@ -0,0 +1,53 @@ +import pyodbc +from ..util_functions.helper import create_success_envelope, create_error_envelope + +class getIFSClass(): + def __init__(self, connection, mapepire=False): + self.conn = connection + self.mapepire = mapepire + """ + Handles user information retrieval and messaging functionalities. + + This class provides methods to interact with the database for retrieving user information + and to send messages to specified users. It supports data retrieval in different formats + (e.g., JSON or tuple), and it enables system messaging with configurable options. + + :ivar conn: Database connection object used for executing queries. + :type conn: Any + """ + def readIFS(self, path_to_read:str, subtrees:bool=True) -> dict[str, str]: + pSubtree = 'NO' + if subtrees: + pSubtree = 'YES' + + sql_query = f""" + SELECT * FROM TABLE ( + QSYS2.IFS_OBJECT_STATISTICS( + START_PATH_NAME => '{path_to_read}', + SUBTREE_DIRECTORIES => '{pSubtree}' + ) + )""" + + + try: + with self.conn.cursor() as cursor: + cursor.execute(sql_query) + rows = cursor.fetchall() + if self.mapepire: + data = rows.get('data', []) if isinstance(rows, dict) else rows + return create_success_envelope(data) + if not rows: + error_msg = f"No Path found for {path_to_read}" + return create_error_envelope(error_msg, func_name="readIFS") + + # Get column names + columns = [column[0] for column in cursor.description] + + + results = [dict(zip(columns, r)) for r in rows] + return create_success_envelope(results) + + + + except Exception as e: + return create_error_envelope(error_msg=str(e), func_name="readIFS") \ No newline at end of file diff --git a/app/iLibrary/src/iLibrary/util_functions/__init__.py b/app/iLibrary/src/iLibrary/util_functions/__init__.py new file mode 100644 index 0000000..129a0de --- /dev/null +++ b/app/iLibrary/src/iLibrary/util_functions/__init__.py @@ -0,0 +1 @@ +from .helper import * \ No newline at end of file diff --git a/app/iLibrary/src/iLibrary/util_functions/helper.py b/app/iLibrary/src/iLibrary/util_functions/helper.py new file mode 100644 index 0000000..4ace6be --- /dev/null +++ b/app/iLibrary/src/iLibrary/util_functions/helper.py @@ -0,0 +1,34 @@ +#-------------------------------------------- +# Helper Functions to get better Error Mails +# -------------------------------------------- +import json +from datetime import datetime, timezone + + +def create_success_envelope(data, message="successful"): + timestamp = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z') + return json.dumps({ + "success": True, + "code": 200, + "message": message, + "metadata": { + "timestamp": timestamp, + "count": len(data) + }, + "data": data, + "error": None + }, indent=4, default=str) + +def create_error_envelope(error_msg:str, func_name:str): + timestamp = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z') + return json.dumps({ + "success": False, + "code": 500, + "message": f"An error occurred in {func_name}", + "metadata": { + "timestamp": timestamp, + "count": 0 + }, + "data": [], + "error": {"details": error_msg} + }, indent=4, default=str) \ No newline at end of file diff --git a/app/iLibrary/src/saveLibrary.py b/app/iLibrary/src/saveLibrary.py deleted file mode 100644 index 2841b29..0000000 --- a/app/iLibrary/src/saveLibrary.py +++ /dev/null @@ -1,455 +0,0 @@ -from _ast import Raise -from os.path import join -import paramiko -import pyodbc -import json -from datetime import datetime, date -from decimal import Decimal -from typing import Union -from pathlib import PureWindowsPath - - -class saveLibrary: - - def saveLibrary(self, - library: str, - saveFileName: str, - dev: str = None, - vol: str = None, - toLibrary: str = None, - description: str = None, - localPath: str = None, - remPath: str = None, - getZip: bool = False, - port: int = None, - remSavf=True, - version: str = None, - max_records: Union[int, str, None] = None, - asp: Union[int, str, None] = None, - waitFile: Union[int, str, None] = None, - share: str = None, - authority: str = None - ) -> bool: - """ - Saves a library to a specified save file, providing options for further customization such - as setting the target release, saving as a zip file, specifying the device, volume, and more. - - :param library: The name of the library to be saved. Must be a valid library name or one of - the predefined options such as '*NONSYS', '*ALLUSR', '*IBM', etc. - :type library: str - :param saveFileName: The name of the save file where the library will be saved. - :type saveFileName: str - :param dev: The target device for the save operation. Defaults to '*SAVF' if not provided. - :type dev: str, optional - :param vol: Specifies the volume to be used. Use ‘*MOUNTED’ to refer to the mounted volume. - :type vol: str, optional - :param toLibrary: Target library where the save file will be temporarily stored. Defaults - to the value of `library` if not specified. - :type toLibrary: str, optional - :param description: An optional description for the save file to be created. - :type description: str, optional - :param localPath: The local path where the save file will be downloaded if `getZip` is set - to True. Must be an absolute path. - :type localPath: str, optional - :param remPath: The remote directory path on the target system to temporarily store the - save file if `getZip` is set to True. Must be an absolute path. - :type remPath: str, optional - :param getZip: A flag that determines whether the save file should be archived into a zip - file and downloaded locally. - :type getZip: bool - :param port: Specifies the port to be used for transferring the save file when `getZip` is - enabled. - :type port: int, optional - :param remSavf: A flag indicating whether the save file should be removed from the remote - target system after a successful save. - :type remSavf: bool - :param version: The target release version for the save operation. Valid values include - ‘*CURRENT’, or specific OS versions like 'V1R1M0', 'V2R3M0', and so on. - :type version: str, optional - :param max_records: Optional parameter for specifying the maximum number of records in - the save file. - :type max_records: Union[int, str, None], optional - :param asp: Auxiliary storage pool (ASP) device number or name if applicable. - :type asp: Union[int, str, None], optional - :param waitFile: The amount of time to wait for file access locks to be released. - :type waitFile: Union[int, str, None], optional - :param share: Specifies the share handling for threads or users accessing the save file. - :type share: str, optional - :param authority: Authority option to set for the save file being saved. - :type authority: str, optional - :return: A boolean indicating whether the library was successfully saved. Returns True on - success or False on failure. - :rtype: bool - """ - # Target Release List - trgList: list = ["V1R1M0", "V1R1M2", "V1R2M0", "V1R3M0", "V2R1M0", "V2R1M1", - "V2R2M0", "V2R3M0", "V3R0M5", "V3R1M0", "V3R2M0", "V3R6M0", - "V3R7M0", "V4R1M0", "V4R2M0", "V4R3M0", "V4R4M0", "V4R5M0", - "V5R1M0", "V5R2M0", "V5R3M0", "V5R4M0", "V6R1M0", "V6R1M1", - "V7R1M0", "V7R2M0", "V7R3M0", "V7R4M0", "V7R5M0", "V7R6M0"] - - # check if something missing from the Arguments - # check if Library is empty or not - if not library: - raise ValueError("A library name is required.") - # check if saveFileName is empty or not - if not saveFileName: - raise ValueError("A save file name is required.") - # check if toLibrary is empty or not - if not toLibrary: - toLibrary = library - # check if user want the SaveFile as ZIP File - if getZip: - if not remPath: - raise ValueError("A remote path is required. Use 'remPath' instead.") - elif remPath[-1] == '/': - remPath = remPath[:-1] - if not localPath: - raise ValueError("A local path is required. Use 'localPath' instead.") - elif localPath[-1] == '/': - localPath = localPath[:-1] - # check wich Version of SaveFile is wanted - if not version in list(trgList): - version = "*CURRENT" - else: - version = version.upper() - command_str: str = f'SAVLIB' - - # check if Library is valid or not - validated_library = self.__validate_max_value(value=library, param_name='library', - str_format=['*NONSYS', '*ALLUSR', '*IBM', '*SELECT', '*USRSPC', - library]) - if validated_library: - command_str += f' LIB({validated_library})' - else: - library_str = str(library) - raise ValueError( - f"The library '{library_str}' is not valid. Must be one of the specified strings or a valid number.") - # check Dev - Device - if not dev in ['*SAVF', '*MEDDFN']: - command_str += f' DEV(*SAVF)' - else: - command_str += f' DEV({dev.upper()})' - if vol is not None and vol == '*MOUNTED': - command_str += f' VOL({vol})' - # starting with mem main Sourcecode of saveLLibrary - if self.__crtsavf(saveFileName, toLibrary, description, max_records=max_records, asp=asp, waitFile=waitFile, - share=share, authority=authority): - # command_str: str = f"SAVLIB LIB({library.strip()}) DEV(*SAVF) SAVF({toLibrary.strip()}/{saveFileName.strip()}) TGTRLS({version.strip()})" - command_str += f" SAVF({toLibrary.strip()}/{saveFileName.strip()}) TGTRLS({version.strip()})" - #print(command_str) - try: - with self.conn.cursor() as cursor: - # execute the Command for creating a Savefile. - cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str)) - if getZip: - try: - remote_temp_savf_path = join(remPath, saveFileName.upper() + '.savf') - - destination_local_path = join(localPath, saveFileName.upper() + '.savf') - command_str = ( - f"CPYTOSTMF FROMMBR('/QSYS.LIB/{toLibrary.upper().strip()}.LIB/{saveFileName.upper().strip()}.FILE') " - f"TOSTMF('{remote_temp_savf_path.strip()}') STMFOPT(*REPLACE)" - ) - - # Execute the command on the remote system - cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str,)) - - if self.__getSavFile(localFilePath=destination_local_path, - remotePath=remote_temp_savf_path, port=port): - rmvCommand = f"QSH CMD('rm -r {remote_temp_savf_path}')" - cursor.execute("CALL QSYS2.QCMDEXC(?)", (rmvCommand)) - else: - raise ValueError("Something went wrong. With downloading the Save File.") - if remSavf: - if not self.removeFile(library=toLibrary, saveFileName=saveFileName): - raise ValueError(f"The Save File {saveFileName} was not successfully removed.") - - except Exception as e: - self.__handle_error(error=e, pgm="saveLibrary - Transfer") - - except Exception as e: - self.__handle_error(error=e, pgm="saveLibrary") - self.conn.rollback() - return False - else: - self.conn.commit() - if getZip: - print(f"File successfully downloaded to: {destination_local_path}") - return True - - print(f"Successfully saved in the Library '{library}' successfully.") - return True - - return False - - # ------------------------------------------------------ - # sub Function: create the Savefile on the AS400 - # ------------------------------------------------------ - def __crtsavf(self, - saveFileName: str, - library: str, - description: str = None, - max_records: Union[int, str, None] = None, - asp: Union[int, str, None] = None, - waitFile: Union[int, str, None] = None, - share: str = None, - authority: str = None - ) -> bool: - """ - Sub-function to create a save file on the IBM i server. - - This function executes the `CRTSAVF` (Create Save File) CL command - to create a new save file in the specified library. This is a - prerequisite for saving a library's contents. - - Args: - saveFileName (str): The name of the save file to be created. - This will be the AS/400 object name. - library (str): The name of the library where the save file will be created. - description (str, optional): A text description for the save file. Defaults to None. - - Returns: - bool: True if the save file was created successfully, False otherwise. - """ - # check is a parameter empty or not - - if not saveFileName: - raise ValueError("A file name is required.") - if not library: - raise ValueError("A library name is required.") - if not description: - description = 'A SaveFile from iLibrary' - - command_str: str = f"CRTSAVF FILE({library.upper().strip()}/{saveFileName.upper().strip()}) TEXT('{description.strip()}')" - - # check max_records for MAXRCDS parameter - if self.__validate_max_value(value=max_records, param_name='max_records', str_format=['*NOMAX'], - max_limit=4293525600) and not None: - command_str += f" MAXRCDS({max_records})" - # check asp for ASP 2147483647 - if self.__validate_max_value(value=asp, param_name='asp', str_format=['*LIBASP'], max_limit=32) and not None: - command_str += f" ASP({asp})" - if self.__validate_max_value(value=waitFile, param_name='waitFile', str_format=['*IMMED', '*CLS'], - max_limit=2147483647) and not None: - command_str += f" WAITFILE({waitFile})" - if self.__validate_max_value(value=share, param_name='share', str_format=['*YES', '*NO']) and not None: - command_str += f" SHARE({share})" - - if authority is not None: - upper_authority = authority.upper() - - # 1. Check for custom authority (not in list AND up to 10 chars) - if upper_authority not in ['*EXCLUDE', '*ALL', '*CHANGE', '*LIBCRTAUT', '*USE'] and len( - upper_authority) <= 10: - # **CORRECTION 1: Use upper_authority here, not the undefined 'auth'** - command_str += f" AUT({upper_authority})" - # The 'pass' statements are redundant and can be removed - - # 2. Add an 'elif' to handle the case where it IS one of the standard values - elif upper_authority in ['*EXCLUDE', '*ALL', '*CHANGE', '*LIBCRTAUT', '*USE']: - command_str += f" AUT({upper_authority})" - - - try: - with self.conn.cursor() as cursor: - # execute the Command for creating a Savefile. - cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str)) - - except Exception as e: - self.__handle_error(error=e, pgm="__crtsavf") - # remove a SAVF if its exists and we got an error - if e.args[0] == 'HY000': - sql = """ - SELECT 1 - FROM QSYS2.SAVE_FILE_INFO - WHERE SAVE_FILE_LIBRARY = ? \ - AND SAVE_FILE = ? - FETCH FIRST 1 ROW ONLY \ - """ - cursor = self.conn.cursor() - cursor.execute(sql, library, saveFileName) - result = cursor.fetchone() - if result is not None: - self.removeFile(library=library, saveFileName=saveFileName) - self.conn.rollback() - raise ValueError(e) - else: - self.conn.commit() - return True - - # -------------------------------------------------------------------------- - # __validate_max_value - Helper Function for checking parameter - # -------------------------------------------------------------------------- - def __validate_max_value(self, - value: Union[int, str, None], - param_name: str, - str_format: list[str], - min_limit: int = 1, - max_limit: int = None - ) -> Union[int, str, bool]: # Includes bool as requested - """ - Validates an input value for 'MAX' type parameters against a custom range. - Handles special strings defined in str_format and numeric values. - - Returns: The validated integer, the standardized special string, or False on failure (if no exception is raised). - Raises: ValueError for invalid string format or out-of-range number. - """ - - # Helper for clear error messages - str_options = ", ".join([f"'{s}'" for s in str_format]) - - # 1. Handle special string - if isinstance(value, str): - upper_value = value.upper() - - for special_value in str_format: - normalized_special_value = special_value.upper() - - if upper_value == special_value.upper() or upper_value == normalized_special_value: - # Found a match! Return the official, fully formatted string. - return special_value - - # 2. Attempt Numeric Conversion (handles int and string-of-int) - if value is not None: - try: - numeric_value = int(value) - except ValueError: - # Value is an invalid string (e.g., 'hello') - raise ValueError( - f"Invalid value for {param_name}. Must be '{str_format}' or a number " - f"between {min_limit} and {max_limit:,}." - ) - else: - # If the value is None - return False - - # 3. Check Numeric Range - if min_limit <= numeric_value <= max_limit: - return numeric_value - else: - # Number is out of range - raise ValueError( - f"Invalid numeric value for {param_name}. Must be between {min_limit} and {max_limit:,}. " - f"Received: {numeric_value}" - ) - - # ------------------------------------------------------ - # getZipFile - getting the Zipfile from the SaveFile - # ------------------------------------------------------ - def __getSavFile(self, - localFilePath: str, - remotePath: str, - port: int = None - ) -> bool: - """ - Downloads a file from the remote IBM i via SFTP. - - This method uses Paramiko to establish a secure shell (SSH) connection and - then an SFTP session to transfer a file from a specified remote location - on the IBM i's IFS to a local path. - - Args: - localFilePath (str): The full path to the file on the remote IBM i's IFS. - remotePath (str): The full path on the local machine where the file - will be saved. For example, '/Users/user/Documents/somefile.savf'. - port (int, optional): The port to connect to the IBMi server. Defaults to None. - - Returns: - bool: True if the file was downloaded successfully, False otherwise. - - Raises: - ValueError: If either the remote_file_path or local_save_path is not provided. - """ - if not localFilePath: - print("Error: A local file path is required.") - return False - if not remotePath: - print("Error: A remote path is required.") - return False - if not port: - port = 2222 - - remotePath = PureWindowsPath(remotePath).as_posix() - ssh_client = paramiko.SSHClient() - - ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - try: - with ssh_client: - ssh_client.connect( - hostname=self.db_host, - username=self.db_user, - password=self.db_password, - port=port - ) - with ssh_client.open_sftp() as ftp_client: - ftp_client.get(remotePath, localFilePath) - return True - - except paramiko.ssh_exception.AuthenticationException as e: - print(f"Authentication failed. Check your username and password: {e}") - return False - except paramiko.ssh_exception.SSHException as e: - print(f"SSH error occurred: {e}") - return False - except FileNotFoundError as e: - print(f"File not found on the remote host: {e}") - return False - - finally: - pass - - def removeFile(self, library: str, saveFileName: str) -> bool: - """ - Removes a save file from the specified library. - - This function executes the system command to delete a save file from an IBM i - system. It connects to the database through a cursor, and attempts to perform - the operation. If an error is encountered during execution, the function - rolls back the transaction and logs the error. On success, the transaction - is committed. - - :param library: The name of the library containing the save file to be removed. - :type library: str - :param saveFileName: The name of the save file to be removed. - :type saveFileName: str - :return: True if the save file is removed successfully, otherwise False. - :rtype: bool - """ - command_str: str = f"DLTF FILE({library.upper()}/{saveFileName.upper()})" - try: - with self.conn.cursor() as cursor: - # execute the Command for deleting a Savefile. - cursor.execute("CALL QSYS2.QCMDEXC(?)", (command_str)) - - except Exception as e: - self.__handle_error(error=e, pgm="removeFile") - self.conn.rollback() - return False - else: - self.conn.commit() - return True - - def __handle_error(self, error, pgm: str): - """ - Handles errors encountered during the execution of a command. - - This method processes an error raised during the execution of a command in a - specific function and extracts detailed error information including SQLSTATE - and the error message. The formatted details are printed to the console for - debugging purposes. - - :param error: The error object encountered during command execution. - :type error: Exception - :param pgm: The name of the function where the error occurred. - :type pgm: str - :return: None - """ - print("-------------------------------------------------------------") - print(f"An error occurred while executing command in function {pgm}:") - sqlstate = error.args[0] - error_message = error.args[1] - - print(f"SQLSTATE: {sqlstate}") - print(f"Message: {error_message}") \ No newline at end of file diff --git a/app/iLibrary/tests/test_user.py b/app/iLibrary/tests/test_user.py new file mode 100644 index 0000000..fe42b7f --- /dev/null +++ b/app/iLibrary/tests/test_user.py @@ -0,0 +1,87 @@ +import pytest +from unittest.mock import patch, MagicMock +import os +from dotenv import load_dotenv +from os.path import join, dirname, abspath +import iLibrary + +# --- Global Path Setup --- +# Find the project root directory (three levels up from 'app/iLibrary/tests') +PROJECT_ROOT = abspath(join(dirname(__file__), '..', '..', '..')) + +# --- Setup Environment for Testing --- +DOTENV_PATH = join(PROJECT_ROOT, '.env') +load_dotenv(DOTENV_PATH) + +# Use a fallback value "ALBEER" if the environment variable is missing +# This prevents the 'AssertionError: assert "ALBEER" == ""' in CI environments +DB_USER = os.environ.get("DB_USER", "ALBEER") +DB_PASSWORD = os.environ.get("DB_PASSWORD", "password") +DB_SYSTEM = os.environ.get("DB_SYSTEM", "localhost") +DB_DRIVER = os.environ.get("DB_DRIVER", "IBM i Access ODBC Driver") + +@pytest.fixture +def mock_user_context(): + """ + Mocks the iLibrary.User class specifically. + """ + mock_user_class = MagicMock() + mock_user_instance = MagicMock() + + # Define a side effect to ensure the mock returns whatever username is requested + # This makes the test resilient to changes in environment variables + def dynamic_user_info(username): + return {"USERNAME": username} + + mock_user_instance.getSingleUserInformation.side_effect = dynamic_user_info + + # Configure the context manager behavior (the 'with' statement) + mock_user_class.return_value.__enter__.return_value = mock_user_instance + + # Patch the 'User' class inside the iLibrary package + with patch('iLibrary.User', mock_user_class): + yield mock_user_class, mock_user_instance + + +def test_get_single_user_info_mapepire(mock_user_context): + """ + Tests the getSingleUserInformation call with mapepire=True. + """ + mock_user_class, user_instance = mock_user_context + TEST_USER = DB_USER + + # Execute the code under test + with iLibrary.User(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=True) as lib: + data = lib.getSingleUserInformation(username=TEST_USER) + + # --- Assertions --- + # 1. Assert constructor received the correct arguments + mock_user_class.assert_called_once_with( + DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=True + ) + + # 2. Assert the method was called with the correct username + user_instance.getSingleUserInformation.assert_called_once_with(username=TEST_USER) + + # 3. Assert the data returned matches the input (ALBEER == ALBEER) + assert data["USERNAME"] == TEST_USER + + +def test_get_single_user_info_odbc(mock_user_context): + """ + Tests initialization and call when Mapepire is DISABLED (ODBC mode). + """ + mock_user_class, user_instance = mock_user_context + TEST_USER = DB_USER + + # Execute with mapepire=False + with iLibrary.User(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=False) as lib: + data = lib.getSingleUserInformation(username=TEST_USER) + + # --- Assertions --- + mock_user_class.assert_called_once_with( + DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=False + ) + + user_instance.getSingleUserInformation.assert_called_once_with(username=TEST_USER) + assert data["USERNAME"] == TEST_USER \ No newline at end of file diff --git a/dev_test.py b/dev_test.py new file mode 100644 index 0000000..00b3de5 --- /dev/null +++ b/dev_test.py @@ -0,0 +1,18 @@ +import json +from os.path import join, dirname +import os +from dotenv import load_dotenv +from iLibrary import Library, User, IFS +from os.path import dirname + +#load ENV file and get the Connection Settings +dotenv_path = join(dirname(__file__), '.env') +load_dotenv(dotenv_path) +DB_DRIVER = os.environ.get("DB_DRIVER") +DB_USER = os.environ.get("DB_USER") +DB_PASSWORD = os.environ.get("DB_PASSWORD") +DB_SYSTEM = os.environ.get("DB_SYSTEM") + + +if __name__ == "__main__": + print('Nothing to do') \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2ece558..81cda03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,6 @@ pyodbc # Library to load environment variables from a .env file python-dotenv - +mapepire-python datetime pdoc \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 93360d8..7fe7905 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [metadata] name = iLibrary -version = 0.0.14 -author = jgh1 +version = 0.0.17 +author = legner.beer author_email = iSave@legner.beer description = iSave - Tools for IBM i library long_description = file: README.md diff --git a/setup.py b/setup.py index 812b7d4..5004cb7 100644 --- a/setup.py +++ b/setup.py @@ -5,8 +5,8 @@ setup( # Note: no 'setuptools.' prefix here name = "iLibrary", - version = "0.0.14", - author = "jgh1", + version = "0.0.17", + author = "legner.beer", author_email = "iLibrary@legner.beer", description = "iLibrary - Tools for IBM i library", long_description = long_description, @@ -19,7 +19,7 @@ classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Operating System :: OS Independent", ], package_dir={"": "app/iLibrary/src"}, @@ -29,5 +29,6 @@ "paramiko", "pyodbc", "python-dotenv", + "mapepire-python" ], ) \ No newline at end of file diff --git a/test.py b/test.py deleted file mode 100644 index fe3ffee..0000000 --- a/test.py +++ /dev/null @@ -1,34 +0,0 @@ -from os.path import join, dirname -import os -from dotenv import load_dotenv -from iLibrary import Library, User -from pathlib import Path -import pyodbc -#load ENV file and get the Connection Settings -dotenv_path = join(dirname(__file__), '.env') -load_dotenv(dotenv_path) -DB_DRIVER = os.environ.get("DB_DRIVER") -DB_USER = os.environ.get("DB_USER") -DB_PASSWORD = os.environ.get("DB_PASSWORD") -DB_SYSTEM = os.environ.get("DB_SYSTEM") -DB_CREDENTIALS = { - "db_user": DB_USER, - "db_password": DB_PASSWORD, - "db_host": DB_SYSTEM, - "db_driver": DB_DRIVER -} -print(pyodbc.drivers()) - -ok = 'Backup Completed Successfully' - -if __name__ == "__main__": - from os.path import dirname - try: - with Library(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER) as lb: - data = lb.getFileInfo(library='ALBEER1') - print(data) - with User(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER) as lb: - data = lb.getAllUsers(wantJson=True) - print(data) - except Exception as e: - print(f"An error occurred: {e}") \ No newline at end of file From 9504db5f1d8061a949ffe33e0f9e3fcd8d354953 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Mon, 20 Apr 2026 08:23:55 +0200 Subject: [PATCH 30/31] Fixxes --- app/iLibrary/src/iLibrary/ifs/ifs_logic.py | 2 +- dev_test.py | 37 +++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/app/iLibrary/src/iLibrary/ifs/ifs_logic.py b/app/iLibrary/src/iLibrary/ifs/ifs_logic.py index e7cad38..5c5ea18 100644 --- a/app/iLibrary/src/iLibrary/ifs/ifs_logic.py +++ b/app/iLibrary/src/iLibrary/ifs/ifs_logic.py @@ -50,4 +50,4 @@ def readIFS(self, path_to_read:str, subtrees:bool=True) -> dict[str, str]: except Exception as e: - return create_error_envelope(error_msg=str(e), func_name="readIFS") \ No newline at end of file + return create_error_envelope(error_msg=str(e), func_name="readIFS") diff --git a/dev_test.py b/dev_test.py index 00b3de5..d38364f 100644 --- a/dev_test.py +++ b/dev_test.py @@ -15,4 +15,39 @@ if __name__ == "__main__": - print('Nothing to do') \ No newline at end of file + + # Path in the IBM i Integrated File System (IFS) to read + REMOTE_PATH_TO_READ: str = '/home/ALBEER' + + # If True, reads all subdirectories recursively + # If False, reads only the specified directory + SUBTREE: bool = False + + # Flag to enable/disable Mapepire connection mode + USE_MAPEPIRE: bool = False + + try: + # Try to establish a connection to the IBM i server + # The IFS class is used as a context manager to ensure proper cleanup + with IFS(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as i: + + # Call the readIFS method to retrieve file system data + # - path_to_read: directory path in the IFS + # - subtrees: whether to include subdirectories + raw_result = i.readIFS( + path_to_read=REMOTE_PATH_TO_READ, + subtrees=SUBTREE + ) + + # Convert the returned JSON string into a Python object (dict/list) + data = json.loads(raw_result) + + # Pretty-print the JSON data with indentation for readability + print(json.dumps(data, indent=2)) + + # Catch and handle any errors that occur during execution + except Exception as e: + # Print a simple error message for debugging purposes + print(f"An error occurred: {e}") + + From fadd7f0ce81cc7f17c63bcc48f9ef83bcac03189 Mon Sep 17 00:00:00 2001 From: Andreas Legner Date: Mon, 18 May 2026 13:40:35 +0200 Subject: [PATCH 31/31] feat(System): adding WRKACTJOB --- .../src/iLibrary/Libr/getInfoForLibrary.py | 2 +- app/iLibrary/src/iLibrary/Libr/saveLibrary.py | 2 +- app/iLibrary/src/iLibrary/Library.py | 2 +- app/iLibrary/src/iLibrary/System/__init__.py | 0 .../src/iLibrary/System/getWrkactjob.py | 93 +++++++++++++++++++ app/iLibrary/src/iLibrary/User.py | 2 +- .../src/iLibrary/Usr/getUserInfoForUser.py | 2 +- app/iLibrary/src/iLibrary/__init__.py | 3 +- app/iLibrary/src/iLibrary/system.py | 93 +++++++++++++++++++ app/iLibrary/tests/test_user.py | 6 +- dev_test.py | 52 +++++------ 11 files changed, 217 insertions(+), 40 deletions(-) create mode 100644 app/iLibrary/src/iLibrary/System/__init__.py create mode 100644 app/iLibrary/src/iLibrary/System/getWrkactjob.py create mode 100644 app/iLibrary/src/iLibrary/system.py diff --git a/app/iLibrary/src/iLibrary/Libr/getInfoForLibrary.py b/app/iLibrary/src/iLibrary/Libr/getInfoForLibrary.py index 2d6a09a..d0dda66 100644 --- a/app/iLibrary/src/iLibrary/Libr/getInfoForLibrary.py +++ b/app/iLibrary/src/iLibrary/Libr/getInfoForLibrary.py @@ -2,7 +2,7 @@ -class getInfoForLibrary: +class GetInfoForLibrary: def __init__(self, connection, mapepire=False): self.conn = connection self.mapepire = mapepire diff --git a/app/iLibrary/src/iLibrary/Libr/saveLibrary.py b/app/iLibrary/src/iLibrary/Libr/saveLibrary.py index d5a5a91..2f29717 100644 --- a/app/iLibrary/src/iLibrary/Libr/saveLibrary.py +++ b/app/iLibrary/src/iLibrary/Libr/saveLibrary.py @@ -4,7 +4,7 @@ from ..util_functions.helper import create_success_envelope, create_error_envelope -class saveLibrary: +class SaveLibrary: def __init__(self, connection, mapepire=False): """ Initializes the saveLibrary parent class. diff --git a/app/iLibrary/src/iLibrary/Library.py b/app/iLibrary/src/iLibrary/Library.py index 09e886f..09db635 100644 --- a/app/iLibrary/src/iLibrary/Library.py +++ b/app/iLibrary/src/iLibrary/Library.py @@ -5,7 +5,7 @@ -class Library(getInfoForLibrary, saveLibrary): +class Library(GetInfoForLibrary, SaveLibrary): """ A class to manage libraries and files on an IBM i system. diff --git a/app/iLibrary/src/iLibrary/System/__init__.py b/app/iLibrary/src/iLibrary/System/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/iLibrary/src/iLibrary/System/getWrkactjob.py b/app/iLibrary/src/iLibrary/System/getWrkactjob.py new file mode 100644 index 0000000..30719a5 --- /dev/null +++ b/app/iLibrary/src/iLibrary/System/getWrkactjob.py @@ -0,0 +1,93 @@ +from ..util_functions.helper import create_success_envelope, create_error_envelope + +class GetWrkActJob: + def __init__(self, connection, mapepire=False): + self.conn = connection + self.mapepire = mapepire + """ + Handles user information retrieval and messaging functionalities. + + This class provides methods to interact with the database for retrieving user information + and to send messages to specified users. It supports data retrieval in different formats + (e.g., JSON or tuple), and it enables system messaging with configurable options. + + :ivar conn: Database connection object used for executing queries. + :type conn: Any + """ + def get_active_jobs(self) -> dict[str, str]: + """ + Retrieves information about active jobs from the system. + + This method queries the database for active job information using the + QSYS2.ACTIVE_JOB_INFO() table function. It processes the retrieved data and + returns it in a formatted envelope. If no active jobs are found, or an error + occurs, an appropriate error envelope is returned. + + Returns: + dict[str, str]: A dictionary representing a success or error envelope. + + Raises: + Exception: If an error occurs during the database query or data processing. + """ + sql_query = "SELECT * FROM TABLE(QSYS2.ACTIVE_JOB_INFO())" + + + try: + with self.conn.cursor() as cursor: + cursor.execute(sql_query) + rows = cursor.fetchall() + if self.mapepire: + data = rows.get('data', []) if isinstance(rows, dict) else rows + return create_success_envelope(data) + if not rows: + error_msg = f"No active jobs found" + return create_error_envelope(error_msg, func_name="getwrkactjob") + + # Get column names + columns = [column[0] for column in cursor.description] + + + results = [dict(zip(columns, r)) for r in rows] + return create_success_envelope(results) + + + + except Exception as e: + return create_error_envelope(error_msg=str(e), func_name="getwrkactjob") + + def get_active_jobs_filter_by_subsystem(self, subsystem:str) -> dict[str, str]: + """ + Retrieves information about active jobs from the system. + This method queries the database for active job information using the + """ + + if not isinstance(subsystem, str): + raise TypeError(f"Parameter 'subsystem' must be a str, got '{type(subsystem).__name__}'") + + sql_query = f""" + SELECT * + FROM TABLE(QSYS2.ACTIVE_JOB_INFO(SUBSYSTEM_LIST_FILTER => ?)) + """ + try: + with self.conn.cursor() as cursor: + cursor.execute(sql_query, (f'{subsystem}',)) + rows = cursor.fetchall() + if self.mapepire: + data = rows.get('data', []) if isinstance(rows, dict) else rows + return create_success_envelope(data) + if not rows: + error_msg = f"No active jobs found" + return create_error_envelope(error_msg, func_name="get_ActiveJob_filter_by_subsystem") + + # Get column names + columns = [column[0] for column in cursor.description] + + + results = [dict(zip(columns, r)) for r in rows] + return create_success_envelope(results) + + + + except Exception as e: + return create_error_envelope(error_msg=str(e), func_name="get_ActiveJob_filter_by_subsystem") + diff --git a/app/iLibrary/src/iLibrary/User.py b/app/iLibrary/src/iLibrary/User.py index 456328c..01c283c 100644 --- a/app/iLibrary/src/iLibrary/User.py +++ b/app/iLibrary/src/iLibrary/User.py @@ -3,7 +3,7 @@ from .Usr.getUserInfoForUser import * from .Usr.sendMSG import * -class User(getUserInfoForUser, sendMSG): +class User(GetUserInfoForUser, sendMSG): """ A class to manage User on IBMi System diff --git a/app/iLibrary/src/iLibrary/Usr/getUserInfoForUser.py b/app/iLibrary/src/iLibrary/Usr/getUserInfoForUser.py index 2958758..f40bf23 100644 --- a/app/iLibrary/src/iLibrary/Usr/getUserInfoForUser.py +++ b/app/iLibrary/src/iLibrary/Usr/getUserInfoForUser.py @@ -1,7 +1,7 @@ import pyodbc from ..util_functions.helper import create_success_envelope, create_error_envelope -class getUserInfoForUser(): +class GetUserInfoForUser(): def __init__(self, connection, mapepire=False): self.conn = connection self.mapepire = mapepire diff --git a/app/iLibrary/src/iLibrary/__init__.py b/app/iLibrary/src/iLibrary/__init__.py index 7907946..8c72746 100644 --- a/app/iLibrary/src/iLibrary/__init__.py +++ b/app/iLibrary/src/iLibrary/__init__.py @@ -1,3 +1,4 @@ from .Library import Library from .User import User -from .IFS import IFS \ No newline at end of file +from .IFS import IFS +from .system import System diff --git a/app/iLibrary/src/iLibrary/system.py b/app/iLibrary/src/iLibrary/system.py new file mode 100644 index 0000000..38ee381 --- /dev/null +++ b/app/iLibrary/src/iLibrary/system.py @@ -0,0 +1,93 @@ +from mapepire_python import connect +import pyodbc +from .System.getWrkactjob import * + +class System(GetWrkActJob): + """ + A class to manage the System on an IBM i system. + + """ + + # ------------------------------------------------------ + # __init__ - initzialise the class + # ------------------------------------------------------ + def __init__(self, db_user: str, db_password: str, db_host: str, db_driver: str, mapepire: bool = False): + """ + Initializes the class attributes for a database connection. + The actual connection is established in the __enter__ method. + + Args: + db_user (str): The user ID for the database connection. + db_password (str): The password for the database user. + db_host (str): The system/host name for the database connection. + db_driver (str): The ODBC driver to be used. + """ + self.db_user = db_user + self.db_host = db_host + self.db_driver = db_driver + self.db_password = db_password + self.mapepire = mapepire + + # ------------------------------------------------------ + # __enter__ - enter to the class + # ------------------------------------------------------ + def __enter__(self) -> 'System': + """ + Establishes the database connection when entering a 'with' block. + """ + try: + if not self.mapepire: + conn_str = ( + f"DRIVER={self.db_driver};" + f"SYSTEM={self.db_host};" + f"UID={self.db_user};" + f"PWD={self.db_password};" + ) + self.conn = pyodbc.connect(conn_str, autocommit=True) + + else: + conn_str = { + "host": self.db_host, + "port": 8076, + "user": self.db_user, + "password": self.db_password, + } + self.conn = connect(conn_str) + super().__init__(self.conn, mapepire=self.mapepire) + return self + + except pyodbc.Error as ex: + sqlstate = ex.args[0] + print(f"Database connection failed with error: {sqlstate}") + raise + except Exception as e: + + print(f"Database connection failed with error: {e}") + raise + + # ------------------------------------------------------ + # __exit__ - leave the class + # ------------------------------------------------------ + def __exit__(self, exc_type, exc_val, exc_tb): + """ + Closes the database connection when exiting a 'with' block. + This method is called automatically, even if an error occurred. + """ + self.iclose() + + + # ------------------------------------------------------ + # iClose - close connection + # ------------------------------------------------------ + def iclose(self): + if not self.conn: + return + + try: + # Both pyodbc and mapepire-python support .close() + # but mapepire MUST have it called to kill background threads + self.conn.close() + except Exception: + pass + finally: + self.conn = None \ No newline at end of file diff --git a/app/iLibrary/tests/test_user.py b/app/iLibrary/tests/test_user.py index fe42b7f..4cdfee4 100644 --- a/app/iLibrary/tests/test_user.py +++ b/app/iLibrary/tests/test_user.py @@ -55,15 +55,15 @@ def test_get_single_user_info_mapepire(mock_user_context): data = lib.getSingleUserInformation(username=TEST_USER) # --- Assertions --- - # 1. Assert constructor received the correct arguments + mock_user_class.assert_called_once_with( DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=True ) - # 2. Assert the method was called with the correct username + user_instance.getSingleUserInformation.assert_called_once_with(username=TEST_USER) - # 3. Assert the data returned matches the input (ALBEER == ALBEER) + assert data["USERNAME"] == TEST_USER diff --git a/dev_test.py b/dev_test.py index d38364f..64df8b0 100644 --- a/dev_test.py +++ b/dev_test.py @@ -2,7 +2,7 @@ from os.path import join, dirname import os from dotenv import load_dotenv -from iLibrary import Library, User, IFS +from iLibrary import Library, User, IFS, System from os.path import dirname #load ENV file and get the Connection Settings @@ -13,41 +13,31 @@ DB_PASSWORD = os.environ.get("DB_PASSWORD") DB_SYSTEM = os.environ.get("DB_SYSTEM") +def getSingleLibraryInfo(): + USE_MAPEPIRE = False -if __name__ == "__main__": - - # Path in the IBM i Integrated File System (IFS) to read - REMOTE_PATH_TO_READ: str = '/home/ALBEER' - - # If True, reads all subdirectories recursively - # If False, reads only the specified directory - SUBTREE: bool = False + try: + # Establish a connection to the IBM i system using the User class + # The context manager ensures the connection is properly opened and closed + with System(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as u: - # Flag to enable/disable Mapepire connection mode - USE_MAPEPIRE: bool = False + # Call the method to retrieve all users from the system + # The result is returned as a JSON string + raw_result = u.get_active_jobs() - try: - # Try to establish a connection to the IBM i server - # The IFS class is used as a context manager to ensure proper cleanup - with IFS(DB_USER, DB_PASSWORD, DB_SYSTEM, DB_DRIVER, mapepire=USE_MAPEPIRE) as i: - - # Call the readIFS method to retrieve file system data - # - path_to_read: directory path in the IFS - # - subtrees: whether to include subdirectories - raw_result = i.readIFS( - path_to_read=REMOTE_PATH_TO_READ, - subtrees=SUBTREE - ) - - # Convert the returned JSON string into a Python object (dict/list) + # Parse the JSON string into a Python object (list/dictionary) data = json.loads(raw_result) + counter = data['metadata'].get('count') - # Pretty-print the JSON data with indentation for readability - print(json.dumps(data, indent=2)) - - # Catch and handle any errors that occur during execution + # Pretty-print the parsed data with indentation for readability + print(json.dumps(data, indent=4)) + print(counter) + # Handle any exceptions that occur during connection or data retrieval except Exception as e: - # Print a simple error message for debugging purposes - print(f"An error occurred: {e}") + # Print the error message for debugging + print(e) + +if __name__ == "__main__": + getSingleLibraryInfo()