├── .coveragerc ├── .flake8 ├── .github └── workflows │ └── python-package.yml ├── .gitignore ├── .pylintrc ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile ├── HACKING.md ├── LICENSE ├── Makefile ├── README.md ├── bin └── sapcli ├── codecov.yml ├── dev-requirements.txt ├── doc ├── commands.md ├── commands │ ├── abapgit.md │ ├── activation.md │ ├── atc.md │ ├── aunit.md │ ├── badi.md │ ├── bsp.md │ ├── businessservice.md │ ├── checkout.md │ ├── class.md │ ├── cts.md │ ├── dataelement.md │ ├── datapreview.md │ ├── ddl.md │ ├── flp.md │ ├── functiongroup.md │ ├── functionmodule.md │ ├── gcts.md │ ├── include.md │ ├── interface.md │ ├── package.md │ ├── program.md │ ├── startrfc.md │ ├── structure.md │ ├── strust.md │ ├── table.md │ └── user.md └── configuration.md ├── mypy.ini ├── requirements.txt ├── sap ├── __init__.py ├── adt │ ├── __init__.py │ ├── abapgit.py │ ├── acoverage.py │ ├── acoverage_statements.py │ ├── annotations.py │ ├── atc.py │ ├── aunit.py │ ├── businessservice.py │ ├── checks.py │ ├── core.py │ ├── cts.py │ ├── dataelement.py │ ├── datapreview.py │ ├── enhancement_implementation.py │ ├── errors.py │ ├── function.py │ ├── marshalling.py │ ├── object_factory.py │ ├── objects.py │ ├── package.py │ ├── programs.py │ ├── repository.py │ ├── search.py │ ├── structure.py │ ├── table.py │ └── wb.py ├── cli │ ├── __init__.py │ ├── abapclass.py │ ├── abapgit.py │ ├── activation.py │ ├── adt.py │ ├── atc.py │ ├── aunit.py │ ├── badi.py │ ├── bsp.py │ ├── checkin.py │ ├── checkout.py │ ├── core.py │ ├── cts.py │ ├── datadefinition.py │ ├── dataelement.py │ ├── datapreview.py │ ├── flp.py │ ├── function.py │ ├── gcts.py │ ├── helpers.py │ ├── include.py │ ├── interface.py │ ├── object.py │ ├── package.py │ ├── program.py │ ├── rap.py │ ├── startrfc.py │ ├── structure.py │ ├── strust.py │ ├── table.py │ ├── user.py │ └── wb.py ├── config.py ├── errors.py ├── flp │ ├── __init__.py │ ├── builder.py │ └── service.py ├── odata │ ├── __init__.py │ ├── connection.py │ └── errors.py ├── platform │ ├── __init__.py │ ├── abap │ │ ├── __init__.py │ │ ├── abapgit.py │ │ ├── ddic.py │ │ └── ddic_builders.py │ └── language.py ├── rest │ ├── __init__.py │ ├── connection.py │ ├── errors.py │ └── gcts │ │ ├── __init__.py │ │ ├── errors.py │ │ ├── remote_repo.py │ │ ├── simple.py │ │ └── sugar.py └── rfc │ ├── __init__.py │ ├── bapi.py │ ├── core.py │ ├── errors.py │ ├── strust.py │ └── user.py ├── sapcli ├── sapcli.bat ├── setup.py └── test ├── __init__.py ├── system ├── config.sh ├── run.sh ├── setup │ └── 00-package.sh └── test_cases │ └── 50-abap_include.sh └── unit ├── __init__.py ├── fixtures_abap.py ├── fixtures_adt.py ├── fixtures_adt_atc.py ├── fixtures_adt_aunit.py ├── fixtures_adt_businessservice.py ├── fixtures_adt_checks.py ├── fixtures_adt_clas.py ├── fixtures_adt_coverage.py ├── fixtures_adt_dataelement.py ├── fixtures_adt_datapreview.py ├── fixtures_adt_enhancement_implementation.py ├── fixtures_adt_function.py ├── fixtures_adt_interface.py ├── fixtures_adt_package.py ├── fixtures_adt_program.py ├── fixtures_adt_repository.py ├── fixtures_adt_structure.py ├── fixtures_adt_table.py ├── fixtures_adt_wb.py ├── fixtures_cli_aunit.py ├── fixtures_cli_checkin.py ├── fixtures_cli_checkout.py ├── fixtures_flp_builder.py ├── fixtures_rfc.py ├── fixtures_sap_rest_error.py ├── fixtures_sap_rest_gcts.py ├── infra.py ├── mock.py ├── runtest.sh ├── test_bin_sapcli.py ├── test_sap_adt_abapgit.py ├── test_sap_adt_annotation.py ├── test_sap_adt_atc.py ├── test_sap_adt_aunit.py ├── test_sap_adt_businessservice.py ├── test_sap_adt_checks.py ├── test_sap_adt_class.py ├── test_sap_adt_connection.py ├── test_sap_adt_coverage.py ├── test_sap_adt_coverage_statements.py ├── test_sap_adt_cts.py ├── test_sap_adt_datadefinition.py ├── test_sap_adt_dataelement.py ├── test_sap_adt_datapreview.py ├── test_sap_adt_enhancement_implementation.py ├── test_sap_adt_errors.py ├── test_sap_adt_function.py ├── test_sap_adt_include.py ├── test_sap_adt_interface.py ├── test_sap_adt_marshalling.py ├── test_sap_adt_object.py ├── test_sap_adt_object_factory.py ├── test_sap_adt_package.py ├── test_sap_adt_program.py ├── test_sap_adt_repository.py ├── test_sap_adt_search.py ├── test_sap_adt_structure.py ├── test_sap_adt_table.py ├── test_sap_adt_wb.py ├── test_sap_cli.py ├── test_sap_cli_abapclass.py ├── test_sap_cli_abapgit.py ├── test_sap_cli_activation.py ├── test_sap_cli_adt.py ├── test_sap_cli_atc.py ├── test_sap_cli_aunit.py ├── test_sap_cli_badi.py ├── test_sap_cli_bsp.py ├── test_sap_cli_checkin.py ├── test_sap_cli_checkout.py ├── test_sap_cli_core.py ├── test_sap_cli_cts.py ├── test_sap_cli_dataelement.py ├── test_sap_cli_datapreview.py ├── test_sap_cli_ddl.py ├── test_sap_cli_flp.py ├── test_sap_cli_function.py ├── test_sap_cli_gcts.py ├── test_sap_cli_helpers.py ├── test_sap_cli_include.py ├── test_sap_cli_interface.py ├── test_sap_cli_object.py ├── test_sap_cli_package.py ├── test_sap_cli_program.py ├── test_sap_cli_rap.py ├── test_sap_cli_startrfc.py ├── test_sap_cli_structure.py ├── test_sap_cli_strust.py ├── test_sap_cli_table.py ├── test_sap_cli_user.py ├── test_sap_cli_wb.py ├── test_sap_config.py ├── test_sap_errors.py ├── test_sap_flp_builder.py ├── test_sap_odata_connection.py ├── test_sap_odata_errors.py ├── test_sap_platform_abap.py ├── test_sap_platform_abap_abapgit.py ├── test_sap_platform_language.py ├── test_sap_rest_connection.py ├── test_sap_rest_errors.py ├── test_sap_rest_gcts.py ├── test_sap_rfc.py ├── test_sap_rfc_bapi.py ├── test_sap_rfc_core.py ├── test_sap_rfc_strust.py ├── test_sap_rfc_strust_exceptions.py └── test_sap_rfc_user.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | include= 3 | sap/* 4 | bin/* 5 | 6 | [report] 7 | exclude_also = 8 | raise NotImplementedError 9 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501 3 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: true 18 | matrix: 19 | python-version: ["3.10", "3.11"] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install -r dev-requirements.txt 31 | python -m pip install -r requirements.txt 32 | - name: Lint 33 | run: | 34 | make lint 35 | - name: Test 36 | run: | 37 | make test report-coverage 38 | - name: Upload Coverage to Codecov 39 | uses: codecov/codecov-action@v1 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ve/ 2 | *.pyc 3 | */__pycache__/ 4 | *.orig 5 | .coverage 6 | .htmlcov/ 7 | .vscode/ 8 | build/ 9 | dist/ 10 | sapcli.egg-info/ 11 | *openrc 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | 3 | language: python 4 | python: 5 | - 3.6 6 | 7 | git: 8 | depth: false 9 | 10 | install: 11 | - pip install -r requirements.txt 12 | - pip install pylint 13 | - pip install flake8 14 | - pip install coverage 15 | - pip install codecov 16 | 17 | script: 18 | - make check PYLINT_BIN=pylint FLAKE8_BIN=flake8 COVERAGE_BIN=coverage 19 | 20 | after_success: 21 | - codecov -t "53d413ee-6b16-47c5-9522-c50e03c65628" -F unittest 22 | 23 | notifications: 24 | email: 25 | - jakub@thefilaks.net 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | The only rule is document and test as much as possible. 4 | 5 | This project tries to have as less dependencies as possible to avoid problems 6 | with deploying. 7 | 8 | Code does not necessarily need to be document, it should be rather self 9 | explanatory. 10 | 11 | ## Commit + Pull Request workflow 12 | 13 | 1. Fork the repository and clone it into your local development machine 14 | 15 | 2. [HACK](HACKING.md) 16 | 17 | 3. Create commits for logical changes - it is better to create more commits 18 | than less 19 | 20 | 4. Use the short commit message to provide an apt description of the change. 21 | Write the short message in the imperative, present tense. 22 | 23 | 5. Use the commit message body to explain your motivation and document your 24 | thinking process (to put it simple - care to explain "why"). Everybody can 25 | see the changes made, so do not try to summarize them unless you changed 26 | dozens of files. In the case you change visible output, it is a good idea to 27 | provide current version and the new one. 28 | 29 | 4. Open a pull request - if you see Merge commits in your PR, you did something 30 | wrong, so please try to get rid of them 31 | 32 | 5. Check GitHub Actions builder 33 | 34 | 6. In case of build failures, please, **amend your commits** - IOW try to avoid adding 35 | new commits fixing your commits to your pull request 36 | 37 | 7. If a reviewer request changes, try to amend existing commits 38 | 39 | 8. When you amend a commit, please add a new line with '--- vX' where X is the 40 | number of version of the commit and describe the changes to the commit below 41 | that line 42 | 43 | 9. To keep history linear and easy to understand, your pull request commits 44 | will be either Squashed or more likely your PR branch will be merged with 45 | Rebase 46 | 47 | 10. When your PR is merged, you need to do **Rebase** too to synchronize your local 48 | master branch - `git pull -r` or `git fetch upstream && git rebase upstream/master` 49 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-alpine 2 | 3 | RUN pip3 install requests 4 | 5 | COPY . /opt/sapcli 6 | 7 | RUN cd /opt/sapcli &&\ 8 | python3 -m pip install -r requirements.txt &&\ 9 | dos2unix /opt/sapcli/bin/sapcli &&\ 10 | chmod +x /opt/sapcli/bin/sapcli 11 | 12 | ENV PATH="/opt/sapcli/bin:${PATH}" 13 | ENV PYTHONPATH="/opt/sapcli:${PYTHONPATH}" 14 | 15 | WORKDIR /var/tmp 16 | 17 | ENTRYPOINT ["sapcli"] 18 | -------------------------------------------------------------------------------- /HACKING.md: -------------------------------------------------------------------------------- 1 | # Hacking 2 | 3 | Start with extending API functionality and use it in your CLI commands - IOW do 4 | not put too much logic into the CLI commands. 5 | 6 | ## Set up 7 | 8 | 1. you need pylint and flake8 which makes sure you follow coding style 9 | 10 | 2. you might consider installing GNU/make which is used to simplify developers 11 | workflow - there is [Makefile](Makefile) including the steps for checking 12 | code style and running tests 13 | 14 | 3. tests are written in python.unittest to avoid unnecessary dependencies and 15 | are stored in the directory [test/](test/) 16 | 17 | 4. all development dependencies are tracked in the file [dev-requirements.txt](dev-requirements.txt) and can be installed with the following command 18 | 19 | ```bash 20 | pip install -r dev-requirements.txt 21 | ``` 22 | 23 | ## Workflow 24 | 25 | 1. Do your changes 26 | 27 | 2. Run linters - either `make lint` or 28 | 29 | ```bash 30 | pylint --rcfile=.pylintrc sap 31 | PYTHONPATH=$(pwd):$PYTHONPATH pylint --rcfile=.pylintrc bin/sapcli 32 | flake8 --config=.flake8 sap 33 | PYTHONPATH=$(pwd):$PYTHONPATH flake8 --config=.flake8 bin/sapcli 34 | ``` 35 | 36 | 3. Run tests - either `make test` or 37 | 38 | ```bash 39 | python -m unittest discover -b -v -s test/unit 40 | ``` 41 | 42 | You can also test subset (or single test case) by providing test case name 43 | pattern: 44 | 45 | ```bash 46 | python -m unittest discover -b -v -s test/unit -k test-name-pattern 47 | ``` 48 | 49 | 4. Check code coverage - run either `make report-coverage` or 50 | 51 | ```bash 52 | coverage run -m unittest discover -b -v -s test/unit 53 | coverage report bin/sapcli $(find sap -type f -name '*.py') 54 | ``` 55 | 56 | You can also use `html` instead of `run` which will create 57 | the report at the path `SOURCE_CODE_DIR/htmlcov/index.html`. 58 | (Of course, the convenience make target exists - `report-coverage-html`). 59 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON_MODULE_DIR=./ 2 | PYTHON_MODULE=sap 3 | PYTHON_BINARIES=bin/sapcli 4 | PYTHON_MODULE_FILES=$(shell find $(PYTHON_MODULE) -type f -name '*.py') 5 | 6 | TESTS_DIR=test 7 | TESTS_UNIT_DIR=$(TESTS_DIR)/unit 8 | TESTS_UNIT_FILES=$(shell find $(TESTS_UNIT_DIR) -type f -name '*.py') 9 | 10 | PYTHON_BIN=python3 11 | 12 | COVERAGE_BIN=coverage 13 | COVERAGE_CMD_RUN=$(COVERAGE_BIN) run 14 | COVERAGE_CMD_REPORT=$(COVERAGE_BIN) report 15 | COVERAGE_REPORT_ARGS=--skip-covered 16 | COVERAGE_CMD_HTML=$(COVERAGE_BIN) html 17 | COVERAGE_HTML_DIR=.htmlcov 18 | COVERAGE_HTML_ARGS=$(COVERAGE_REPORT_ARGS) -d $(COVERAGE_HTML_DIR) 19 | COVERAGE_REPORT_FILES=$(PYTHON_BINARIES) $(PYTHON_MODULE_FILES) 20 | 21 | PYTEST_MODULE=unittest 22 | PYTEST_PARAMS=discover -b -v -s $(TESTS_UNIT_DIR) 23 | 24 | PYLINT_BIN ?= pylint 25 | PYLINT_RC_FILE=.pylintrc 26 | PYLINT_PARAMS=--output-format=parseable --reports=no 27 | 28 | FLAKE8_BIN ?= flake8 29 | FLAKE8_CONFIG_FILE=.flake8 30 | FLAKE8_PARAMS= 31 | 32 | MYPY_BIN ?= mypy 33 | MYPY_CONFIG_FILE=mypy.ini 34 | MYPY_PARAMS= 35 | 36 | .PHONY: run_pylint 37 | run_pylint: 38 | $(PYLINT_BIN) --rcfile=$(PYLINT_RC_FILE) $(PYLINT_PARAMS) $(PYTHON_MODULE) 39 | PYTHONPATH=$(PYTHON_MODULE_DIR):$$PYTHONPATH $(PYLINT_BIN) --rcfile=$(PYLINT_RC_FILE) $(PYLINT_PARAMS) $(PYTHON_BINARIES) 40 | 41 | .PHONY: run_flake8 42 | run_flake8: 43 | $(FLAKE8_BIN) --config=$(FLAKE8_CONFIG_FILE) $(FLAKE8_PARAMS) $(PYTHON_MODULE) 44 | PYTHONPATH=$(PYTHON_MODULE_DIR):$$PYTHONPATH $(FLAKE8_BIN) --config=$(FLAKE8_CONFIG_FILE) $(FLAKE8_PARAMS) $(PYTHON_BINARIES) 45 | 46 | .PHONY: run_mypy 47 | run_mypy: 48 | PYTHONPATH=$(PYTHON_MODULE_DIR):$$PYTHONPATH $(MYPY_BIN) --config-file=$(MYPY_CONFIG_FILE) $(MYPY_PARAMS) $(PYTHON_BINARIES) $(PYTHON_MODULE_FILES) 49 | 50 | .PHONY: lint 51 | lint: run_pylint run_flake8 run_mypy 52 | @ echo "*** Linters done ***" 53 | 54 | .PHONY: test 55 | test: 56 | $(PYTHON_BIN) -m $(PYTEST_MODULE) $(PYTEST_PARAMS) 57 | 58 | .coverage: $(COVERAGE_REPORT_FILES) $(TESTS_UNIT_FILES) 59 | $(MAKE) test PYTHON_BIN="$(COVERAGE_CMD_RUN)" 60 | 61 | .PHONY: report-coverage 62 | report-coverage: .coverage 63 | @ $(COVERAGE_CMD_REPORT) $(COVERAGE_REPORT_ARGS) $(COVERAGE_REPORT_FILES) 64 | 65 | .PHONY: report-coverage-html 66 | report-coverage-html: .coverage 67 | @ echo "Generating HTML code coverage report ..." 68 | @ $(COVERAGE_CMD_HTML) $(COVERAGE_HTML_ARGS) $(COVERAGE_REPORT_FILES) 69 | @ echo "Report: file://$$(pwd)/$(COVERAGE_HTML_DIR)/index.html" 70 | 71 | .PHONY: system-test 72 | system-test: 73 | export PATH=$$(pwd):$$PATH; cd test/system && ./run.sh 74 | 75 | .PHONY: check 76 | check: lint report-coverage 77 | 78 | .PHONY: clean 79 | clean: 80 | rm -rf .coverage $(COVERAGE_HTML_DIR) 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build Status](https://github.com/jfilak/sapcli/actions/workflows/python-package.yml/badge.svg) 2 | [![codecov](https://codecov.io/gh/jfilak/sapcli/branch/master/graph/badge.svg)](https://codecov.io/gh/jfilak/sapcli) 3 | 4 | # SAP CLI 5 | 6 | Command line interface to SAP products 7 | 8 | This tool provides command line interface for ADT which should help you to 9 | build your CI tools. 10 | 11 | This tool also provides a limited set of RFC Functionality for the cases 12 | where ADT is no sufficient or possible. 13 | 14 | ## Installation and usage 15 | 16 | First of all you need Python3 (>=3.10) and then you need python-request module. 17 | Nothing else because ADT works on level HTTP. 18 | 19 | ### Ubuntu 20 | 21 | ```bash 22 | sudo apt-get install -y git python3 python3-requests python3-openssl python3-venv 23 | git clone https://github.com/jfilak/sapcli.git 24 | cd sapcli 25 | python3 -m venv ve 26 | . ve/bin/activate 27 | pip install -r requirements.txt 28 | ./sapcli --help 29 | ``` 30 | 31 | ### Enable RFC features 32 | 33 | sapcli uses [PyRFC](https://sap.github.io/PyRFC/intro.html) which provides Python API for communication 34 | over SAP NetWeaver RFC. 35 | 36 | Please, follow the installation official install instructions at: 37 | [https://sap.github.io/PyRFC/install.html](https://sap.github.io/PyRFC/install.html) 38 | 39 | #### Linux hints 40 | 41 | It is not necessary to modify */etc/ld.so.conf.d/nwrfcsdk.conf* as you can 42 | just set the environment variable LD\_LIBRARY\_PATH. 43 | 44 | The required libraries are compiled the way you can executed them on any x86-64 45 | GNU/Linux, thus you can use the libraries located on your Application server. 46 | 47 | ## Features 48 | 49 | The primary goal was to enable ABAP Unit testing 50 | 51 | ```bash 52 | sapcli aunit run class zcl_foo --output junit4 53 | sapcli aunit run program zfoo_report --output junit4 54 | sapcli aunit run package '$local_package' --output junit4 55 | ``` 56 | 57 | , ATC checks triggering and installation of [abapGit](https://github.com/larshp/abapGit) 58 | which is delivered as a single source ABAP program (report, SE38 thing). 59 | 60 | ```bash 61 | sapcli package create '$abapgit' 'git for ABAP by Lars' 62 | sapcli program create 'zabapgit' 'github.com/larshp/abapGit' '$abapgit' 63 | curl https://raw.githubusercontent.com/abapGit/build/master/zabapgit.abap | sapcli program write 'zabapgit' - --activate 64 | ``` 65 | 66 | See the complete list of supported operations in [doc/commands.md](doc/commands.md) 67 | 68 | ## Usage 69 | 70 | You must provide the tool with hostname, client, user and password. It is 71 | possible to use either command line parameters or environment variables. 72 | 73 | You can prepare a configuration file like the following: 74 | 75 | ```bash 76 | cat > .npl001.sapcli.openrc << _EOF 77 | export SAP_USER=DEVELOPER 78 | export SAP_PASSWORD=Down1oad 79 | export SAP_ASHOST=vhcalpnlci 80 | export SAP_CLIENT=001 81 | export SAP_PORT=8000 82 | export SAP_SSL=no 83 | _EOF 84 | ``` 85 | 86 | and the you can source the configuration file in your shell to avoid the need 87 | to repeat the configuration on command line parameters: 88 | 89 | ```bash 90 | source .npl001.sapcli.openrc 91 | 92 | sapcli package create '$abapgit' 'git for ABAP by Lars' 93 | sapcli program create zabapgit 'github.com/larshp/abapGit' '$abapgit' 94 | sapcli aunit run class zabapgit 95 | ``` 96 | 97 | The tool asks only for user and password if missing. All other parameters 98 | either have own default value or causes fatal error if not provided. 99 | 100 | Find the complete documentation in [doc/configuration.md](doc/configuration.md) 101 | 102 | ### RFC usage 103 | 104 | When using the RFC features you have to provide the following additional 105 | parameters: 106 | 107 | * __--sysnr__ which can be provided as the environment value **SAP\_SYSNR** 108 | 109 | ## For developers 110 | 111 | Your contribution is more than welcome! Nothing is worse than the code that does not exist. 112 | 113 | Have a look into [CONTRIBUTING guide](CONTRIBUTING.md), if you are not sure how to start. 114 | 115 | And even seasoned GiHub contributors might consider checking out [HACKING guide](HACKING.md). 116 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfilak/sapcli/229d58a3efeffec7b3b4afaf4fb069f8d83b9c5a/codecov.yml -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | coverage>=5.4 2 | pylint>=2.6.0 3 | flake8>=3.8.4 4 | mypy>=0.930 5 | types-PyYAML>=6.0.1 6 | types-requests>=2.26.2 7 | -------------------------------------------------------------------------------- /doc/commands.md: -------------------------------------------------------------------------------- 1 | # sapcli supported commands 2 | 3 | 1. [program](commands/program.md) - Reports aka programs 4 | 2. [include](commands/include.md) - Includes 5 | 3. [functiongroup](commands/functiongroup.md) - Function Groups 6 | 4. [functionmodule](commands/functionmodule.md) - Function Modules 7 | 5. [class](commands/class.md) - Object Oriented Classes 8 | 6. [interface](commands/interface.md) - Object Oriented Interfaces 9 | 7. [ddl](commands/ddl.md) - Core Data Services aka CDS views 10 | 8. [package](commands/package.md) - Development Classes aka Packages 11 | 9. [aunit](commands/aunit.md) - ABAP Unit Test framework 12 | 10. [atc](commands/atc.md) - ATC aka Source Code Inspector 13 | 11. [cts](commands/cts.md) - Change Transport System 14 | 12. [gcts](commands/gcts.md) - git enabled Change Transport System 15 | 13. [checkout](commands/checkout.md) - Source Code Management System 16 | 14. [datapreview](commands/datapreview.md) - Simple ABAP OSQL queries 17 | 15. [startrfc](commands/startrfc.md) - Run arbitrary RFC enabled Function Modules 18 | 16. [user](commands/user.md) - Create, read, and modify AS ABAP users 19 | 17. [bsp](commands/bsp.md) - Management of BSP applications 20 | 18. [flp](commands/flp.md) - Fiori Launchpad 21 | 19. [rap](commands/businessservice.md) - RAP Business Services 22 | 20. [strust](commands/strust.md) - SSL Certificates 23 | 21. [dataelement](commands/dataelement.md) - ABAP DDIC Data Elements 24 | 22. [structure](commands/structure.md) - ABAP DDIC structures 25 | 23. [table](commands/table.md) - ABAP DDIC transparent tables 26 | 24. [badi](commands/badi.md) - New style (Enhancements) BAdI operations -------------------------------------------------------------------------------- /doc/commands/abapgit.md: -------------------------------------------------------------------------------- 1 | # Abapgit 2 | 3 | ## Link 4 | 5 | Links existing package to remote repository in abapgit 6 | 7 | ```bash 8 | sapcli abapgit link [--remote-user USER] [--remote-password PASSWORD] [--branch BRANCH] [--corrnr CORRNR] PACKAGE URL 9 | ``` 10 | 11 | If the parameter `--branch` is not present, the `refs/heads/master` value is used. 12 | 13 | ## Pull 14 | 15 | Pulls content from remote repositopry to a package. 16 | 17 | After the operation completes, repository status is reported. If status is Error or Aborted, errors and warning from repository are printed. 18 | 19 | ```bash 20 | sapcli abapgit pull [--remote-user USER] [--remote-password PASSWORD] [--branch BRANCH] [--corrnr CORRNR] PACKAGE 21 | ``` 22 | 23 | If the parameter `--branch` is not present, the `refs/heads/master` value is used. 24 | -------------------------------------------------------------------------------- /doc/commands/activation.md: -------------------------------------------------------------------------------- 1 | # Activation 2 | 3 | 1. [inactiveobjects](#inactiveobjects) 4 | 5 | ## inactiveobjects 6 | 7 | Deals with Inactive Objects 8 | 9 | 1. [list](#list) 10 | 11 | ### list 12 | 13 | List of all inactive objects 14 | 15 | ```bash 16 | sapcli activation inactiveobjects list 17 | ``` 18 | -------------------------------------------------------------------------------- /doc/commands/badi.md: -------------------------------------------------------------------------------- 1 | # BAdI (Enhancement Spot/Implementation) 2 | 3 | 1. [list](#list) 4 | 1. [set-active](#set-active) 5 | 6 | 7 | ## list 8 | 9 | List BAdI implementations of a particular Enhancement Implementation 10 | 11 | ```bash 12 | sapcli badi [-i|--enhancement_implementation ENHO] [list] 13 | ``` 14 | 15 | * _--enhancement_implementation ENHO_ name of the ENHO object (Enhancement Implementation) 16 | 17 | ## set-active 18 | 19 | Change the definition of ABAP DDIC transparent table. 20 | 21 | ```bash 22 | saplci badi [-i|--enhancement_implementation ENHO] set-active [-b|--badi NAME] [-a|--activate] [true|false] 23 | ``` 24 | 25 | * _--enhancement_implementation_ ENHO name of the ENHO object (Enhancement Implementation) 26 | * _--name BADI_ name of the BAdI implementation 27 | * _--activate_ run activation of the enhancement implementation after the change 28 | * _[true|false]_ is the set value 29 | -------------------------------------------------------------------------------- /doc/commands/bsp.md: -------------------------------------------------------------------------------- 1 | # BSP Applications 2 | 3 | 1. [upload](#upload) 4 | 2. [stat](#stat) 5 | 3. [delete](#delete) 6 | 7 | ## upload 8 | 9 | Uploads the packed javascript sources for the BSP application `APP123` in package `SOME_PACKAGE`. 10 | Be aware of: 11 | 12 | * package and transport have to exist. 13 | * zipped application codebase have to be preprocessed (minified), e.g. by 14 | [`@sap/ux-ui5-tooling`](https://www.npmjs.com/package/@sap/ux-ui5-tooling) 15 | * if the application does not exist yet, it will be created automatically 16 | 17 | ```bash 18 | sapcli bsp upload \ 19 | --app=/some/dire/app.zip \ 20 | --package=SOME_PACKAGE \ 21 | --bsp=APP123 \ 22 | --corrnr=C50K000167 \ 23 | ``` 24 | 25 | ## stat 26 | 27 | Prints basic set of BSP application attributes. Returned exit could be interpreted as: 28 | 29 | * `0` - application found 30 | * `10` - application not found 31 | 32 | ```bash 33 | sapcli bsp stat --bsp=APP123 34 | ``` 35 | 36 | which results in output similar to: 37 | 38 | ``` 39 | Name :APP123 40 | Package :SOME_PACKAGE 41 | Description :Application Description 42 | ``` 43 | 44 | ## delete 45 | 46 | Deletes a BSP application 47 | 48 | ```bash 49 | sapcli bsp delete --bsp=APP123 --corrnr=C50K000167 50 | ``` 51 | -------------------------------------------------------------------------------- /doc/commands/businessservice.md: -------------------------------------------------------------------------------- 1 | # rap 2 | 3 | 1. [definition activate](#activate) 4 | 2. [biding publish](#publish) 5 | 6 | ## definition 7 | 8 | ### activate 9 | 10 | Activates the give Business Service Definition 11 | 12 | ```bash 13 | sapcli rap definition activate NAME [NAME [NAME ...]] 14 | ``` 15 | 16 | **Parameters**: 17 | - `NAME`: A business service definition name to activate 18 | 19 | ## binding 20 | 21 | ### publish 22 | 23 | Publishes a desired oData service name or oData service version in the corresponding service binding 24 | 25 | ```bash 26 | sapcli rap binding publish BINDING_NAME [--service SERVICE_NAME] [--version SERVICE_VERSION] 27 | ``` 28 | 29 | **Parameters**: 30 | - `BINDING_NAME`: A business service binding whose service definition will be published 31 | - `--service SERVICE_NAME`: Name of the service to publish 32 | - `--version SERVICE_VERSION`: Version of the service to publish 33 | 34 | If no SERVICE\_NAME nor SERVICE\_VERSION is supplied and the binding contains only 35 | one service, that service will be published by default. Otherwise, the command 36 | will exit with non-0 code and action will be performed. 37 | 38 | If SERVICE\_NAME or SERVICE\_VERSION or both values are supplied, a first service 39 | matching the give parameters will be published. If there is no such a service, 40 | the operation will be aborted and sapcli will exit with non-0 code. 41 | -------------------------------------------------------------------------------- /doc/commands/checkout.md: -------------------------------------------------------------------------------- 1 | # checkout 2 | 3 | This set of commands is intended for reading and writing whole packages. 4 | Fetches all source codes of the given class and stores them in local files. 5 | 6 | File format and names should be compatible with [abapGit](https://github.com/larshp/abapGit). 7 | 8 | ```bash 9 | sapcli checkout class zcl_hello_world 10 | ``` 11 | 12 | Fetches source codes of the given program and stores it a local file. 13 | 14 | ```bash 15 | sapcli checkout program z_hello_world 16 | ``` 17 | 18 | Fetches source codes of the given interface and stores it a local file. 19 | 20 | ```bash 21 | sapcli checkout interface zif_hello_world 22 | ``` 23 | 24 | Fetches objects of given function group and stores objects' source codes to a local files. 25 | 26 | ```bash 27 | sapcli checkout function_group zhello_world 28 | ``` 29 | 30 | Fetches source codes of classes, programs and interfaces of the given package 31 | and stores them in corresponding files in a local file system directory. 32 | 33 | The new directory is populated with the file _.abapgit.xml_ which has the format 34 | recognized by [abapGit](https://github.com/larshp/abapGit). 35 | 36 | ```bash 37 | sapcli checkout package '$hello_world' [directory] [--recursive] [--starting-folder DIR] 38 | ``` 39 | 40 | * _directory_ the name of a new directory to checkout the given package into; 41 | if not provided, the package name is used instead 42 | 43 | * _--starting-folder_ forces sapcli to create the corresponding object files in 44 | the given directory; by default, sapcli uses the directory `src` 45 | 46 | * _--recursive_ forces sapcli to download also the sub-packages into sub-directories 47 | 48 | -------------------------------------------------------------------------------- /doc/commands/class.md: -------------------------------------------------------------------------------- 1 | # Class 2 | 3 | 1. [create](#create-1) 4 | 2. [write](#write-1) 5 | 3. [activate](#activate-1) 6 | 4. [read](#read-1) 7 | 5. [attributes](#attributes) 8 | 6. [execute](#execute) 9 | 10 | ## create 11 | 12 | Creates a public final global class of the given name with the given 13 | description in the given package. 14 | 15 | ```bash 16 | sapcli class create ZCL_HELLOWORLD "Class description" '$PACKAGE' 17 | ``` 18 | 19 | ## write 20 | 21 | Changes main source code of the given class without activation 22 | 23 | ``` 24 | sapcli class write [OBJECT_NAME|-] [FILE_PATH+|-] [--corrnr TRANSPORT] [--activate] [--ignore-errors] [--warning-errors] 25 | ``` 26 | 27 | * _OBJECT\_NAME_ either class name or - when it should be deduced from FILE\_PATH 28 | * _FILE\_PATH_ if OBJECT\_NAME is not -, single file path or - for reading _stdin_; otherwise space separated list of file paths 29 | * _--corrnr TRANSPORT_ specifies CTS Transport Request Number if needed 30 | * _--activate_ activate after finishing the write operation 31 | * _--ignore-errors_ continue activating objects ignoring errors 32 | * _--warning-errors_ treat activation warnings as errors 33 | 34 | Changes definitions source code of the given class without activation 35 | 36 | ```bash 37 | sapcli class write "ZCL_HELLOWORLD" --type definitions zcl_helloworld.definitions.abap 38 | ``` 39 | 40 | Changes implementations source code of the given class without activation 41 | 42 | ```bash 43 | sapcli class write "ZCL_HELLOWORLD" --type implementations zcl_helloworld.implementations.abap 44 | ``` 45 | 46 | Changes test classes source code of the given class without activation 47 | 48 | ```bash 49 | sapcli class write "ZCL_HELLOWORLD" --type testclassess zcl_helloworld.testclasses.abap 50 | ``` 51 | 52 | ## activate 53 | 54 | Activates the given class. 55 | 56 | ``` 57 | sapcli class activate [--ignore-errors] [--warning-errors] NAME NAME ... 58 | ``` 59 | 60 | ## read 61 | 62 | Download main source codes of the given public class 63 | 64 | ```bash 65 | sapcli class read ZCL_HELLOWORLD 66 | ``` 67 | 68 | Downloads definitions source codes of the given public class 69 | 70 | ```bash 71 | sapcli class read ZCL_HELLOWORLD --type definitions 72 | ``` 73 | 74 | Downloads implementations source codes of the given public class 75 | 76 | ```bash 77 | sapcli class read ZCL_HELLOWORLD --type implementations 78 | ``` 79 | 80 | Downloads test classes source codes of the given public class 81 | 82 | ```bash 83 | sapcli class read ZCL_HELLOWORLD --type testclasses 84 | ``` 85 | 86 | ## attributes 87 | 88 | Prints out some attributes of the given class 89 | 90 | ```bash 91 | sapcli class attributes ZCL_HELLOWORLD 92 | ``` 93 | 94 | Supported attributes: 95 | * Name 96 | * Description 97 | * Responsible 98 | * Package 99 | 100 | ## execute 101 | 102 | Executes the class if it implements the `if_oo_adt_classrun~main` method and prints the raw output 103 | 104 | ```bash 105 | sapcli class execute ZCL_HELLOWORLD 106 | ``` -------------------------------------------------------------------------------- /doc/commands/cts.md: -------------------------------------------------------------------------------- 1 | # Change Transport System (CTS) 2 | 3 | 1. [list](#list) 4 | 2. [create](#create) 5 | 3. [release](#release) 6 | 5. [reassign](#reassign) 7 | 4. [delete](#delete) 8 | 9 | ## list 10 | 11 | Get list of CTS requests 12 | 13 | ```bash 14 | sapcli cts list {transport,task} [--recursive|--recursive|...] [--owner login] 15 | ``` 16 | 17 | ## create 18 | 19 | Create a CTS request - either Transport or Transport Task 20 | 21 | ```bash 22 | sapcli cts create [transport,task] [--description DESCRIPTION] [--target TARGET] 23 | ``` 24 | 25 | ## release 26 | 27 | Release CTS request - either Transport or Transport Task 28 | 29 | ```bash 30 | sapcli cts release [transport,task] [--recursive] $number 31 | ``` 32 | 33 | When applied to a transport, the parameter *--recursive* causes that 34 | unreleased tasks of the given transport are released first. 35 | 36 | ## reassign 37 | 38 | Change owner of a CTS request - either Transport or Transport Task 39 | 40 | ```bash 41 | sapcli cts reassign [transport,task] [--recursive] NUMBER OWNER 42 | ``` 43 | 44 | When applied to a transport, the parameter *--recursive* causes that 45 | unreleased tasks of the given transport are reassigned too. 46 | 47 | ## delete 48 | 49 | Delete a CTS request - either Transport or Transport Task 50 | 51 | ```bash 52 | sapcli cts delete [transport,task] [--recursive] NUMBER 53 | ``` 54 | 55 | When applied to a transport, the parameter *--recursive* causes that 56 | unreleased tasks of the given transport are deleted too. 57 | -------------------------------------------------------------------------------- /doc/commands/dataelement.md: -------------------------------------------------------------------------------- 1 | # Data Element 2 | 3 | - [Data Element](#data-element) 4 | - [define](#define) 5 | 6 | ## define 7 | 8 | Define an ABAP DDIC Data Element. 9 | 10 | ```bash 11 | sapcli dataelement define DATA_ELEMENT_NAME --type=domain|predefinedAbapType [--corrnr TRANSPORT] [--activate] [--no-error-existing] [--domain_name] [--data_type] [--data_type_length] [--data_type_decimals] [--label_short] [--label_medium] [--label_long] [--label_heading] 12 | ``` 13 | 14 | * _DATA\_ELEMENT\_NAME_ specifying the name of the data element 15 | * _--type [domain|predefinedAbapType]_ type kind 16 | * _--domain\_name_ domain name (e.g. BUKRS) [default = ''] - mandatory in case the _--type_=domain **(optional)** 17 | * _--data\_type_ data type (e.g. CHAR) [default = ''] - mandatory in case the _--type_=predefinedAbapType **(optional)** 18 | * _--data\_type\_length_ data type length (e.g. 5) [default = '0'] **(optional)** 19 | * _--data\_type\_decimals_ data type decimals (e.g. 3) [default = '0'] **(optional)** 20 | * _--label\_short_ short label [default = ''] **(optional)** 21 | * _--label\_medium_ medium label [default = ''] **(optional)** 22 | * _--label\_long_ long label [default = ''] **(optional)** 23 | * _--label\_heading_ heading label [default = ''] **(optional)** 24 | * _--corrnr TRANSPORT_ specifies CTS Transport Request Number **(optional)** 25 | * _--activate_ activate after finishing the data element modification **(optional)** 26 | * _--no-error-existing_ do not fail if data element already exists **(optional)** 27 | -------------------------------------------------------------------------------- /doc/commands/datapreview.md: -------------------------------------------------------------------------------- 1 | # Datapreview 2 | 3 | Wrappers for ADT SQL console. 4 | 5 | ## osql 6 | 7 | Executes a oneliner OSQL statement in ABAP system and prints out the results. 8 | 9 | **Example**: 10 | 11 | ```bash 12 | sapcli datapreview osql "select mandt cccategory from t000" 13 | ``` 14 | 15 | the output for ABAP Trial would be: 16 | 17 | ``` 18 | MANDT | CCCATEGORY 19 | 000 | S 20 | 001 | C 21 | ``` 22 | 23 | **Parameters**: 24 | 25 | ```bash 26 | sapcli datapreview osql STATEMENT [--output human|json] [--rows (100)] [--noaging] [--noheadings] 27 | ``` 28 | 29 | * _STATEMENT_ the executed ABAP OpenSQL statement 30 | 31 | * _--output_ either human friendly or JSON output format; where the default is human 32 | 33 | * _--rows_ "up to"; where the default is 100 34 | 35 | * _--noaging_ turns of data aging 36 | 37 | * _--noheadings_ removes column names from the human friendly output 38 | 39 | -------------------------------------------------------------------------------- /doc/commands/ddl.md: -------------------------------------------------------------------------------- 1 | # DataDefinition (CDS) 2 | 3 | 1. [read](#read) 4 | 2. [activate](#activate) 5 | 6 | ## activate 7 | 8 | Activates the given CDS views in the given order 9 | 10 | ```bash 11 | sapcli ddl activate ZCDS1 ZCDS2 ZCDS3 ... 12 | ``` 13 | 14 | ## read 15 | 16 | Download main source codes of the given public CDS view 17 | 18 | ```bash 19 | sapcli ddl read ZCDS1 20 | ``` 21 | -------------------------------------------------------------------------------- /doc/commands/flp.md: -------------------------------------------------------------------------------- 1 | # Fiori Launchpad 2 | 3 | 1. [init](#init) 4 | 5 | ## init 6 | 7 | Initializes the Fiori Launchpad based on the YAML configuration file 8 | 9 | ```bash 10 | sapcli flp init --config ./config.yml 11 | ``` 12 | 13 | Example configuration: 14 | 15 | ```yaml 16 | catalogs: 17 | - title: Custom Catalog 18 | id: ZCUSTOM_CATALOG 19 | target_mappings: 20 | - title: My Reporting App 21 | semantic_object: MyReporting 22 | semantic_action: display 23 | url: /sap/bc/ui5_ui5/sap/ZMY_REPORT # path to the BSP app on the SAP system 24 | ui5_component: zmy.app.reporting # UI5 app id defined in manifest.json 25 | tiles: 26 | - title: My Reporting App 27 | id: ZMY_REPORTING 28 | icon: sap-icon://settings 29 | semantic_object: MyReporting 30 | semantic_action: display 31 | groups: 32 | - title: Custom Group 33 | id: ZCUSTOM_GROUP 34 | tiles: 35 | - title: My Reporting App 36 | catalog_id: ZCUSTOM_CATALOG 37 | catalog_tile_id: ZMY_REPORTING # this has to match one of the catalogs->tiles->id property 38 | ``` 39 | -------------------------------------------------------------------------------- /doc/commands/functiongroup.md: -------------------------------------------------------------------------------- 1 | # Function Group 2 | 3 | - [Function Group](#function-group) 4 | - [create](#create) 5 | - [write](#write) 6 | - [activate](#activate) 7 | - [read group](#read-group) 8 | - [Function Group Include](#function-group-include) 9 | - [create](#create-1) 10 | - [write](#write-1) 11 | - [activate](#activate-1) 12 | - [read group](#read-group-1) 13 | 14 | ## create 15 | 16 | Creates a function group of the given name with the given description in the 17 | given package. 18 | 19 | ```bash 20 | sapcli functiongroup create ZFG_PARENT "Class description" '$PACKAGE' 21 | ``` 22 | 23 | ## write 24 | 25 | Changes main source code of the given function group. 26 | 27 | ``` 28 | sapcli functiongroup write [FUNCTION_GROUP_NAME|-] [FILE_PATH+|-] [--corrnr TRANSPORT] [--activate] [--ignore-errors] [--warning-errors] 29 | ``` 30 | 31 | * _FUNCITON\_GROUP\_NAME_ either function group name or - when it should be deduced from FILE\_PATH 32 | * _FILE\_PATH_ if FUNCTION\_GROUP\_NAME is not -, a single file path or - for reading _stdin_; otherwise space separated list of file paths 33 | * _--corrnr TRANSPORT_ specifies CTS Transport Request Number if needed 34 | * _--activate_ activate after finishing the write operation 35 | * _--ignore-errors_ continue activating objects ignoring errors 36 | * _--warning-errors_ treat activation warnings as errors 37 | 38 | ## activate 39 | 40 | Activates the given function group. 41 | 42 | ```bash 43 | sapcli functiongroup activate ZFG_PARENT 44 | ``` 45 | 46 | ## read group 47 | 48 | Download main source codes of the given function group 49 | 50 | ```bash 51 | sapcli functiongroup read ZFG_PARENT 52 | ``` 53 | 54 | ## Function Group Include 55 | 56 | ### create 57 | 58 | Creates a function group of the given name with the given description in the 59 | given package. 60 | 61 | ```bash 62 | sapcli functiongroup include create ZFG_PARENT ZFGI_HELLO_WORLD "Function Group Include description" 63 | ``` 64 | 65 | ### write 66 | 67 | Changes main source code of the given function group. 68 | 69 | ``` 70 | sapcli functiongroup include write [FUNCTION_GROUP_NAME] [FUNCTION_GROUP_INCLUDE_NAME] [FILE_PATH|-] [--corrnr TRANSPORT] [--activate] [--ignore-errors] [--warning-errors] 71 | ``` 72 | 73 | * _FUNCITON\_GROUP\_NAME_ function group name 74 | * _FUNCITON\_GROUP\_INCLUDE\_NAME_ function group include name 75 | * _FILE\_PATH_ a single file path or - for reading _stdin_ 76 | * _--corrnr TRANSPORT_ specifies CTS Transport Request Number if needed 77 | * _--activate_ activate after finishing the write operation 78 | * _--ignore-errors_ continue activating objects ignoring errors 79 | * _--warning-errors_ treat activation warnings as errors 80 | 81 | ### activate 82 | 83 | Activates the given function group. 84 | 85 | ```bash 86 | sapcli functiongroup include activate ZFG_PARENT ZFGI_HELLO_WORLD 87 | ``` 88 | 89 | ### read group 90 | 91 | Download main source codes of the given function group 92 | 93 | ```bash 94 | sapcli functiongroup include read ZFG_PARENT ZFGI_HELLO_WORLD 95 | ``` 96 | -------------------------------------------------------------------------------- /doc/commands/functionmodule.md: -------------------------------------------------------------------------------- 1 | # Function Module 2 | 3 | - [Function Module](#function-module) 4 | - [create](#create) 5 | - [write](#write) 6 | - [chattr](#chattr) 7 | - [activate](#activate) 8 | - [read](#read) 9 | 10 | ## create 11 | 12 | Creates a function module in the given function group of the given name with 13 | the given description. 14 | 15 | ```bash 16 | sapcli functionmodule create ZFG_PARENT Z_FUNCTION_MODULE "Class description" 17 | ``` 18 | 19 | ## write 20 | 21 | Changes main source code of the given function module. 22 | 23 | ``` 24 | sapcli functionmodule write [GROUP_NAME|-] [OBJECT_NAME|-] [FILE_PATH+|-] [--corrnr TRANSPORT] [--activate] [--ignore-errors] [--warning-errors] 25 | ``` 26 | 27 | * _GROUP\_NAME_ either function group name or - when it should be deduced from FILE\_PATH 28 | * _OBJECT\_NAME_ either founction module name or - when it should be deduced from FILE\_PATH 29 | * _FILE\_PATH_ if OBJECT\_NAME is not -, single file path or - for reading _stdin_; otherwise space separated list of file paths 30 | * _--corrnr TRANSPORT_ specifies CTS Transport Request Number if needed 31 | * _--activate_ activate after finishing the write operation 32 | * _--ignore-errors_ continue activating objects ignoring errors 33 | * _--warning-errors_ treat activation warnings as errors 34 | 35 | ## chattr 36 | 37 | Changes attributes of the given function module. 38 | 39 | ```bash 40 | sapcli functionmodule chattr "ZFG_PARENT" "Z_FUNCTION_MODULE [--processing_type normal|rfc] [--corrnr TRANSPORT] 41 | ``` 42 | 43 | * _--processing_type [normal|rfc]_ could be used to make RFC enabled 44 | * _--corrnr TRANSPORT_ specifies CTS Transport Request Number if needed 45 | 46 | ## activate 47 | 48 | Activates the given function module. 49 | 50 | ```bash 51 | sapcli functionmodule activate ZFG_PARENT Z_FUNCTION_MODULE 52 | ``` 53 | 54 | ## read 55 | 56 | Download main source codes of the given function module 57 | 58 | ```bash 59 | sapcli functionmodule read ZFG_PARENT Z_FUNCTION_MODULE 60 | ``` 61 | -------------------------------------------------------------------------------- /doc/commands/include.md: -------------------------------------------------------------------------------- 1 | # Includes 2 | 3 | 1. [create](#create) 4 | 2. [write](#write) 5 | 3. [activate](#activate) 6 | 4. [read](#read) 7 | 8 | ## create 9 | 10 | Create executable program 11 | 12 | ```bash 13 | sapcli include create "ZHELLOWORLD_INC" "Just a description" '$TMP' 14 | ``` 15 | 16 | ## write 17 | 18 | Change code of an executable program without activation. 19 | 20 | ``` 21 | sapcli include write [OBJECT_NAME|-] [FILE_PATH+|-] [--corrnr TRANSPORT] [--activate] [--ignore-errors] [--warning-errors] 22 | ``` 23 | 24 | * _OBJECT\_NAME_ either include name or - when it should be deduced from FILE\_PATH 25 | * _FILE\_PATH_ if OBJECT\_NAME is not -, single file path or - for reading _stdin_; otherwise space separated list of file paths 26 | * _--corrnr TRANSPORT_ specifies CTS Transport Request Number if needed 27 | * _--activate_ activate after finishing the write operation 28 | * _--ignore-errors_ continue activating objects ignoring errors 29 | * _--warning-errors_ treat activation warnings as errors 30 | 31 | ## activate 32 | 33 | Activate an executable program. 34 | 35 | ```bash 36 | sapcli include activate [--ignore-errors] [--warning-errors] [--master ZHELLOWORLD] NAME NAME ... 37 | ``` 38 | 39 | * _--master PROGRAM_ sets the master program for include activation 40 | * _--ignore-errors_ continue activating objects ignoring errors 41 | * _--warning-errors_ treat activation warnings as errors 42 | 43 | ## read 44 | 45 | Download source codes 46 | 47 | ```bash 48 | sapcli include read ZHELLOWORLD_INC 49 | ``` 50 | -------------------------------------------------------------------------------- /doc/commands/interface.md: -------------------------------------------------------------------------------- 1 | # Interfaces 2 | 3 | 1. [create](#create) 4 | 2. [write](#write) 5 | 3. [activate](#activate) 6 | 4. [read](#read) 7 | 8 | ## create 9 | 10 | Creates a public interface of the given name with the given 11 | description in the given package. 12 | 13 | ```bash 14 | sapcli interface create ZIF_GREETER "Interface description" '$PACKAGE' 15 | ``` 16 | 17 | ## write 18 | 19 | Changes source code of the given interfaces without activation 20 | 21 | ```bash 22 | sapcli interface write [OBJECT_NAME|-] [FILE_PATH+|-] [--corrnr TRANSPORT] [--activate] [--ignore-errors] [--warning-errors] 23 | ``` 24 | 25 | * _OBJECT\_NAME_ either interface name or - when it should be deduced from FILE\_PATH 26 | * _FILE\_PATH_ if OBJECT\_NAME is not -, single file path or - for reading _stdin_; otherwise space separated list of file paths 27 | * _--corrnr TRANSPORT_ specifies CTS Transport Request Number if needed 28 | * _--activate_ activate after finishing the write operation 29 | * _--ignore-errors_ continue activating objects ignoring errors 30 | * _--warning-errors_ treat activation warnings as errors 31 | 32 | ## activate 33 | 34 | Activates the given interface 35 | 36 | ```bash 37 | sapcli interface activate [--ignore-errors] [--warning-errors] NAME NAME ... 38 | ``` 39 | 40 | * _--ignore-errors_ continue activating objects ignoring errors 41 | * _--warning-errors_ treat activation warnings as errors 42 | 43 | ## read 44 | 45 | Download main source codes of the given public interface 46 | 47 | ```bash 48 | sapcli interface read ZIF_GREETER 49 | ``` 50 | -------------------------------------------------------------------------------- /doc/commands/package.md: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | 1. [create](#create) 4 | 2. [list](#list) 5 | 3. [check](#check) 6 | 4. [stat](#stat) 7 | 8 | ## create 9 | 10 | Creates non-transportable packages 11 | 12 | ```bash 13 | sapcli package create [--super-package SUPER_PKG] [--app-component APP_COMP] [--software-component SW_COMP] [--transport-layer TR_LAYER] [--no-error-existing] NAME DESCRIPTION 14 | ``` 15 | 16 | **Parameters**: 17 | - `--super-package SUPER_PKG`: Name of the parent package. **(optional)** 18 | - `--app-component APP_COMP`: Name of assigned Application Component. **(optional)** 19 | - `--software-component SW_COMP`: Name of assigned Software Component. **(optional)** 20 | - `--transport-layer TR_LAYER`: Name of assigned Transport Layer. **(optional)** 21 | - `--no-error-existing`: Do not exit with non-0 exit code if the package already exists. **(optional)** 22 | - `NAME`: Name of the newly created package. Do not forget to escape $ in shell to avoid ENV variable evaluation. 23 | - `DESCRIPTION`: Description of the newly created package. Do not forget to use " if you need more words. 24 | 25 | ## list 26 | 27 | List sub-packages and object of the given package 28 | 29 | ```bash 30 | sapcli package list \$tests [--recursive] 31 | ``` 32 | 33 | If the parameter `--recursive` is present, the command prints out contents of 34 | sub-packages too. 35 | 36 | ## check 37 | 38 | Run all available standard ADT checks for all objects of the give package. 39 | 40 | ```bash 41 | sapcli package check \$productive_code 42 | ``` 43 | 44 | ## stat 45 | 46 | Prints basic set of package attributes. Returned exit could be interpreted as: 47 | * `0` - package found 48 | * `10` - package not found 49 | 50 | ```bash 51 | sapcli package stat \$productive_code 52 | ``` 53 | 54 | which results in output similar to: 55 | ``` 56 | Name :PROD_BSP_APS 57 | Active :active 58 | Application Component :APP-COMP-XY 59 | Software Component :SW-COMP-XY 60 | Transport Layer : 61 | Package Type :development 62 | ``` 63 | -------------------------------------------------------------------------------- /doc/commands/program.md: -------------------------------------------------------------------------------- 1 | # Programs 2 | 3 | - [Programs](#programs) 4 | - [create](#create) 5 | - [write](#write) 6 | - [activate](#activate) 7 | - [read](#read) 8 | 9 | ## create 10 | 11 | Create executable program 12 | 13 | ```bash 14 | sapcli program create "ZHELLOWORLD" "Just a description" '$TMP' 15 | ``` 16 | 17 | ## write 18 | 19 | Change code of an executable program without activation. 20 | 21 | ``` 22 | sapcli program write [OBJECT_NAME|-] [FILE_PATH+|-] [--corrnr TRANSPORT] [--activate] [--ignore-errors] [--warning-errors] 23 | ``` 24 | 25 | * _OBJECT\_NAME_ either program name or - when it should be deduced from FILE\_PATH 26 | * _FILE\_PATH_ if OBJECT\_NAME is not -, single file path or - for reading _stdin_; otherwise space separated list of file paths 27 | * _--corrnr TRANSPORT_ specifies CTS Transport Request Number if needed 28 | * _--activate_ activate after finishing the write operation 29 | * _--ignore-errors_ continue activating objects ignoring errors 30 | * _--warning-errors_ treat activation warnings as errors 31 | 32 | ## activate 33 | 34 | Activate an executable program. 35 | 36 | ```bash 37 | sapcli program activate [--ignore-errors] [--warning-errors] NAME NAME ... 38 | ``` 39 | 40 | * _--ignore-errors_ continue activating objects ignoring errors 41 | * _--warning-errors_ treat activation warnings as errors 42 | 43 | ## read 44 | 45 | Download source codes 46 | 47 | ```bash 48 | sapcli program read ZHELLOWORLD 49 | ``` 50 | 51 | -------------------------------------------------------------------------------- /doc/commands/startrfc.md: -------------------------------------------------------------------------------- 1 | # startrfc 2 | 3 | This command allows you to run an arbitrary RFC enabled Function Module. 4 | 5 | ```bash 6 | sapcli startrfc --output={human,json} RFC_FUNCTION_MODULE {JSON_PARAMETERS,-} \ 7 | [-I|--integer param:value] [-S|--string param:value] 8 | [-F|--file param:path] 9 | ``` 10 | 11 | * _--output_ allows you to specify format of the output 12 | * _human_ specifies format which suites human readers 13 | * _json_ specifies output in JSON 14 | 15 | * _RFC\_FUNCION\_MODULE_ name of the executed Function Module 16 | 17 | * _JSON\_PARAMETERS_ the call paremeters in the form of JSON object serialized 18 | into string; if - , then JSON string is read from standard input 19 | 20 | * -I | --integer: allows you to pass a numeric parameter of the executed RFC 21 | Function Module as a command line parameter. The value will overwrite value 22 | provided in JSON\_PARAMETERS or will be added if the parameter is not in present 23 | JSON\_PARAMETERS 24 | 25 | * -S | --string: allows you to pass a text parameter of the executed RFC 26 | Function Module as a command line parameter. The value will overwrite value 27 | provided in JSON\_PARAMETERS or will be added if the parameter is not in present 28 | JSON\_PARAMETERS 29 | 30 | * -F | --file: allows you to pass a binary parameter of the executed RFC 31 | Function Module as a command line parameter. The value path is used to open 32 | a file and its contents will be used as value of the RFC param. 33 | 34 | * -c | --result-checker: enables analysis of returned response 35 | * _raw_ the default value which does not do any analysis, just prints out the 36 | formatted response 37 | * _bapi_ stops printing out the retrieved response and instead tries to get 38 | the member *RETURN* from the response, expects the value is a table of the 39 | ABAP type *bapiret2* and prints out messages - if error message is 40 | found, only the error message is printed out and the process exists with 41 | non-0 exit code. 42 | 43 | * -R | --response-file: holds a file path where the complete response of the 44 | executed function module will be stored regardles of the result-checker's 45 | verdict. The format of the file is taken from the parameter '--output'. 46 | 47 | ## Example: human readable output of STFC\_CONNECTION 48 | 49 | Run the function module checking connection to ABAP Trial system deployed in 50 | a docker container. 51 | 52 | ```bash 53 | sapcli --ashost 172.17.0.2 --sysnr 00 --client 001 --user DEVELOPER --password Down1oad \ 54 | startrfc STFC_CONNECTION '{"REQUTEXT":"ping"}' 55 | ``` 56 | 57 | If everything goes as expected you sould see the following outptu: 58 | 59 | ``` 60 | {'ECHOTEXT': 'ping', 61 | 'RESPTEXT': 'SAP R/3 Rel. 752 Sysid: NPL Date: 20200223 Time: ' 62 | '231340 Logon_Data: 001/DEVELOPER/E'} 63 | ``` 64 | 65 | ## Example: JSON output of RFC\_READ\_TABLE 66 | 67 | This example demonstrates JSON format output which we sends through a pipe to 68 | **jq** which then filters out unimportant information. 69 | 70 | The example will print out all users of the client 001. 71 | 72 | 73 | ```bash 74 | sapcli --ashost 172.17.0.2 --sysnr 00 --client 001 --user DEVELOPER --password Down1oad \ 75 | startrfc --output=json RFC_READ_TABLE '{"QUERY_TABLE":"USR02","FIELDS":["BNAME"]}' \ 76 | | jq -r ".DATA[].WA" | tr -s ' ' 77 | ``` 78 | 79 | If everything goes as expected you should see the following output: 80 | 81 | ``` 82 | BWDEVELOPER 83 | DDIC 84 | DEVELOPER 85 | SAP* 86 | ``` 87 | 88 | ## Warning 89 | 90 | This command is not available if [PyRFC](https://sap.github.io/PyRFC/index.html) 91 | is not properly installed and you will not even see the parameter in the output 92 | of _sapcli --help_. 93 | -------------------------------------------------------------------------------- /doc/commands/structure.md: -------------------------------------------------------------------------------- 1 | # Structure 2 | 3 | - [Structure](#structure) 4 | - [create](#create) 5 | - [write](#write) 6 | - [activate](#activate) 7 | - [read](#read) 8 | 9 | ## create 10 | 11 | Create ABAP DDIC structure. 12 | 13 | ```bash 14 | sapcli structure create [--corrnr TRANSPORT] "STRUCTURE_NAME" "Description" "PACKAGE_NAME" 15 | ``` 16 | 17 | * _--corrnr TRANSPORT_ specifies CTS Transport Request Number **(optional)** 18 | 19 | ## write 20 | 21 | Change the definition of ABAP DDIC structure. 22 | 23 | ```bash 24 | sapcli structure write [STRUCTURE_NAME|-] [FILE_PATH+|-] [--corrnr TRANSPORT] [-a|--activate] [--ignore-errors] [--warning-errors] 25 | ``` 26 | 27 | * _STRUCTURE\_NAME_ specifying the name of the structure or `-` to deduce it from the file name specified by FILE\_PATH 28 | * _FILE\_PATH_ if TABLE\_NAME is not `-`, single file path or `-` for reading _stdin_; otherwise space separated list of file paths 29 | * _--corrnr TRANSPORT_ specifies CTS Transport Request Number **(optional)** 30 | * _--ignore-errors_ continue activating objects ignoring errors **(optional)** 31 | * _--warning-errors_ treat activation warnings as errors **(optional)** 32 | 33 | ## activate 34 | 35 | Activate ABAP DDIC structure. 36 | 37 | ```bash 38 | sapcli structure activate [--ignore-errors] [--warning-errors] STRUCTURE_NAME ... 39 | ``` 40 | 41 | * _--ignore-errors_ continue activating objects ignoring errors **(optional)** 42 | * _--warning-errors_ treat activation warnings as errors **(optional)** 43 | 44 | ## read 45 | 46 | Get the definition of ABAP DDIC structure. 47 | 48 | ```bash 49 | sapcli structure read STRUCTURE_NAME 50 | ``` 51 | -------------------------------------------------------------------------------- /doc/commands/table.md: -------------------------------------------------------------------------------- 1 | # Table 2 | 3 | 1. [create](#create) 4 | 2. [write](#write) 5 | 3. [activate](#activate) 6 | 4. [read](#read) 7 | 8 | ## create 9 | 10 | Create ABAP DDIC transparent table. 11 | 12 | ```bash 13 | sapcli table create [--corrnr TRANSPORT] "TABLE_NAME" "Description" "PACKAGE_NAME" 14 | ``` 15 | 16 | * _--corrnr TRANSPORT_ specifies CTS Transport Request Number **(optional)** 17 | 18 | ## write 19 | 20 | Change the definition of ABAP DDIC transparent table. 21 | 22 | ```bash 23 | saplci table write [TABLE_NAME|-] [FILE_PATH+|-] [--corrnr TRANSPORT] [-a|--activate] [--ignore-errors] [--warning-errors] 24 | ``` 25 | 26 | * _TABLE\_NAME_ specifying the name of the table or `-` to deduce it from the file name specified by FILE\_PATH 27 | * _FILE\_PATH_ if TABLE\_NAME is not `-`, single file path or `-` for reading _stdin_; otherwise space separated list of file paths 28 | * _--corrnr TRANSPORT_ specifies CTS Transport Request Number **(optional)** 29 | * _--ignore-errors_ continue activating objects ignoring errors **(optional)** 30 | * _--warning-errors_ treat activation warnings as errors **(optional)** 31 | 32 | ## activate 33 | 34 | Activate ABAP DDIC transparent table. 35 | 36 | ```bash 37 | sapcli table activate [--ignore-errors] [--warning-errors] TABLE_NAME ... 38 | ``` 39 | 40 | * _--ignore-errors_ continue activating objects ignoring errors **(optional)** 41 | * _--warning-errors_ treat activation warnings as errors **(optional)** 42 | 43 | ## read 44 | 45 | Get the definition of ABAP DDIC transparent table. 46 | 47 | ```bash 48 | sapcli table read TABLE_NAME 49 | ``` 50 | -------------------------------------------------------------------------------- /doc/commands/user.md: -------------------------------------------------------------------------------- 1 | # user 2 | 3 | This command set allows you to create, read, and modify users in AS ABAP 4 | systems. This functinaly requires RFC connectivity. 5 | 6 | 1. [details](#details) 7 | 2. [create](#create) 8 | 3. [change](#change) 9 | 10 | ## details 11 | 12 | Prints out very limited list of user parameters retrieved from the configured 13 | system. 14 | 15 | ```bash 16 | sapcli user details USERNAME 17 | ``` 18 | 19 | **Parameters**: 20 | - `USERNAME`: the specified user 21 | 22 | ## create 23 | 24 | Creates the specified user with the given parameters. 25 | 26 | ```bash 27 | sapcli user create [--type Dialog|Service|System] [--new-password PASSWORD] USERNAME 28 | ``` 29 | 30 | **Parameters**: 31 | - `USERNAME`: the created user 32 | - `--new-password PASSWORD`: the created user's new password 33 | - `--type Dialog|Service|System`: the type of the created user 34 | 35 | ## change 36 | 37 | Modifies the specified user with the given parameters. 38 | Currently, it can change only the password. 39 | 40 | ```bash 41 | sapcli user modify [--new-password PASSWORD] USERNAME 42 | ``` 43 | 44 | **Parameters**: 45 | - `USERNAME`: the specified user 46 | - `--new-password PASSWORD`: the user's new password 47 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | disable_error_code = no-redef, import -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.20.0 2 | pyodata==1.7.0 3 | PyYAML==6.0.1 4 | -------------------------------------------------------------------------------- /sap/__init__.py: -------------------------------------------------------------------------------- 1 | """Basic SAP modules infrastructure 2 | 3 | The sub-modules rfc and adt should be independent on each other. 4 | The sub-modules should not depend on any cli functionality. 5 | All configuration must be achieved via dependency injection. 6 | 7 | This module provides cli neutral functionality. 8 | """ 9 | 10 | import os 11 | import logging 12 | 13 | from sap.config import config_get # noqa: F401 14 | 15 | 16 | def get_logger(): 17 | """Returns the common logger object. Don't use for standard output""" 18 | 19 | logger = logging.getLogger() 20 | 21 | env_level = os.environ.get('SAPCLI_LOG_LEVEL', None) 22 | if env_level: 23 | logging.basicConfig() 24 | logger.setLevel(int(env_level)) 25 | 26 | return logger 27 | 28 | 29 | __all__ = [ 30 | "get_logger", 31 | ] 32 | -------------------------------------------------------------------------------- /sap/adt/__init__.py: -------------------------------------------------------------------------------- 1 | """Base classes for ADT functionality modules""" 2 | 3 | from sap.adt.core import Connection # noqa: F401 4 | from sap.adt.function import FunctionGroup, FunctionModule, FunctionInclude # noqa: F401 5 | from sap.adt.objects import ADTObject, ADTObjectType, ADTCoreData, OrderedClassMembers # noqa: F401 6 | from sap.adt.objects import Class, Interface, DataDefinition # noqa: F401 7 | from sap.adt.programs import Program, Include # noqa: F401 8 | from sap.adt.package import Package # noqa: F401 9 | from sap.adt.aunit import AUnit # noqa: F401 10 | from sap.adt.acoverage import ACoverage # noqa: F401 11 | from sap.adt.repository import Repository # noqa: F401 12 | from sap.adt.datapreview import DataPreview # noqa: F401 13 | from sap.adt.businessservice import ServiceDefinition, ServiceBinding # noqa: F401 14 | from sap.adt.table import Table # noqa: F401 15 | from sap.adt.enhancement_implementation import EnhancementImplementation # noqa: F401 16 | from sap.adt.structure import Structure # noqa: F401 17 | from sap.adt.dataelement import DataElement # noqa: F401 18 | -------------------------------------------------------------------------------- /sap/adt/errors.py: -------------------------------------------------------------------------------- 1 | """ADT error types""" 2 | 3 | import re 4 | 5 | from sap.errors import SAPCliError 6 | 7 | 8 | ADT_EXCEPTION_XML_FRAGMENT = '''\ 9 | ''' 10 | 11 | 12 | class ADTError(SAPCliError): 13 | """Errors reported by ADT tools""" 14 | 15 | def __init__(self, namespace, typ, message): 16 | super().__init__() 17 | 18 | self.namespace = namespace 19 | self.type = typ 20 | self.message = message 21 | 22 | def __repr__(self): 23 | return f'{self.namespace}.{self.type}' 24 | 25 | def __str__(self): 26 | return f'{self.type}: {self.message}' 27 | 28 | 29 | class ExceptionResourceAlreadyExists(ADTError): 30 | """Thin wrapper for the class type of ADTErrors""" 31 | 32 | def __init__(self, message): 33 | super().__init__('com.sap.adt', self.__class__.__name__, message) 34 | 35 | def __str__(self): 36 | return f'{self.message}' 37 | 38 | 39 | class ExceptionResourceNotFound(ADTError): 40 | """Thin wrapper for the class type of ADTErrors""" 41 | 42 | def __init__(self, message): 43 | super().__init__('com.sap.adt', self.__class__.__name__, message) 44 | 45 | def __str__(self): 46 | return f'{self.message}' 47 | 48 | 49 | class ExceptionResourceCreationFailure(ADTError): 50 | """Thin wrapper for the class type of ADTErrors""" 51 | 52 | def __init__(self, message): 53 | super().__init__('com.sap.adt', self.__class__.__name__, message) 54 | 55 | def __str__(self): 56 | return f'{self.message}' 57 | 58 | 59 | class ExceptionCheckinFailure(SAPCliError): 60 | """Wrapper for checkin errors""" 61 | 62 | def __init__(self, message): 63 | super().__init__() 64 | self.message = message 65 | 66 | def __str__(self): 67 | return f'{self.message}' 68 | 69 | 70 | class ADTConnectionError(SAPCliError): 71 | """Wrapper for ADT connection errors""" 72 | 73 | def __init__(self, host, port, ssl, message): 74 | super().__init__() 75 | msg = f'[HOST:"{host}", PORT:"{port}", SSL:"{ssl}"] Error: ' 76 | if 'Errno -5' in message: 77 | msg += 'Name resolution error. Check the HOST configuration.' 78 | elif 'Errno 111' in message: 79 | msg += 'Cannot connect to the system. Check the HOST and PORT configuration.' 80 | else: 81 | msg += message 82 | self.message = msg 83 | 84 | def __str__(self): 85 | return f'ADT Connection error: {self.message}' 86 | 87 | 88 | class ExceptionResourceSaveFailure(ADTError): 89 | """Thin wrapper for the class type of ADTErrors""" 90 | 91 | def __init__(self, message): 92 | super().__init__('com.sap.adt', self.__class__.__name__, message) 93 | 94 | def __str__(self): 95 | return f'{self.message}' 96 | 97 | 98 | def new_adt_error_from_xml(xmldata): 99 | """Parses the xml data and create the correct instance. 100 | 101 | Returns None, if the given xmldata does not represent an ADT Exception. 102 | """ 103 | 104 | if not xmldata.startswith(ADT_EXCEPTION_XML_FRAGMENT): 105 | return None 106 | 107 | namespace = re.match('.*.*', xmldata)[1] 108 | typ = re.match('.*.*', xmldata)[1] 109 | message = re.match('.*([^<]*).*', xmldata)[1] 110 | 111 | for subclass in ADTError.__subclasses__(): 112 | if subclass.__name__ == typ: 113 | return subclass(message) 114 | 115 | return ADTError(namespace, typ, message) 116 | -------------------------------------------------------------------------------- /sap/adt/object_factory.py: -------------------------------------------------------------------------------- 1 | """Create instances of ADT object proxies by various names""" 2 | 3 | from typing import Callable, Dict, List, Optional, cast 4 | 5 | import sap.adt 6 | import sap.adt.core 7 | from sap.errors import SAPCliError 8 | 9 | 10 | ADTObjectBuilderType = Callable[[sap.adt.core.Connection, str], object] 11 | ADTObjectBuilderDictType = Dict[str, ADTObjectBuilderType] 12 | 13 | 14 | class ADTObjectFactory: 15 | """Factory producing ADT object Proxies. 16 | """ 17 | 18 | def __init__(self, connection: sap.adt.core.Connection, builders: Optional[ADTObjectBuilderDictType] = None): 19 | self._connection = connection 20 | if builders: 21 | self._builders = builders 22 | else: 23 | self._builders = cast(ADTObjectBuilderDictType, {}) 24 | 25 | def register(self, typ: str, producer: ADTObjectBuilderType, overwrite: bool = False) -> None: 26 | """Registers ADT object builder""" 27 | 28 | if not overwrite and typ in self._builders: 29 | raise SAPCliError(f'Object type builder was already registered: {typ}') 30 | 31 | self._builders[typ] = producer 32 | 33 | def make(self, typ: str, name: str) -> object: 34 | """Accepts object type name and object name and returns 35 | instance of ADT Object proxy. 36 | """ 37 | 38 | try: 39 | ctor = cast(ADTObjectBuilderType, self._builders[typ]) 40 | except KeyError as ex: 41 | raise SAPCliError(f'Unknown ADT object type: {typ}') from ex 42 | 43 | return ctor(self._connection, name) 44 | 45 | def get_supported_names(self) -> List[str]: 46 | """List of known object type names""" 47 | 48 | return cast(List[str], self._builders.keys()) 49 | 50 | 51 | def human_names_factory(connection: sap.adt.core.Connection) -> ADTObjectFactory: 52 | """Returns an instance of factory making ADT object proxies 53 | based on human readable ADT object types. 54 | """ 55 | 56 | types = { 57 | 'program': sap.adt.Program, 58 | 'program-include': sap.adt.programs.make_program_include_object, 59 | 'class': sap.adt.Class, 60 | 'package': sap.adt.Package 61 | } 62 | 63 | return ADTObjectFactory(connection, cast(ADTObjectBuilderDictType, types)) 64 | -------------------------------------------------------------------------------- /sap/adt/search.py: -------------------------------------------------------------------------------- 1 | """Wraps ADT search functionality""" 2 | 3 | from sap.adt.objects import ADTObjectReferences 4 | import sap.adt.marshalling 5 | 6 | 7 | class ADTSearch: 8 | """ADT Search functionality""" 9 | 10 | def __init__(self, connection): 11 | self._connection = connection 12 | 13 | def quick_search(self, term: str, max_results: int = 5) -> ADTObjectReferences: 14 | """Performs the quick object search""" 15 | 16 | resp = self._connection.execute( 17 | 'GET', 18 | 'repository/informationsystem/search', 19 | params={ 20 | 'operation': 'quickSearch', 21 | 'maxResults': max_results, 22 | 'query': term 23 | } 24 | ) 25 | 26 | results = ADTObjectReferences() 27 | marshal = sap.adt.marshalling.Marshal() 28 | marshal.deserialize(resp.text, results) 29 | 30 | return results 31 | -------------------------------------------------------------------------------- /sap/adt/structure.py: -------------------------------------------------------------------------------- 1 | """ABAP Structure ADT functionality module""" 2 | 3 | from sap.adt.objects import ADTObject, ADTObjectType, xmlns_adtcore_ancestor, ADTObjectSourceEditor 4 | 5 | 6 | class Structure(ADTObject): 7 | """ABAP Structure""" 8 | 9 | OBJTYPE = ADTObjectType( 10 | 'TABL/DS', 11 | 'ddic/structures', 12 | xmlns_adtcore_ancestor('blue', 'http://www.sap.com/wbobj/blue'), 13 | 'application/vnd.sap.adt.structures.v2+xml', 14 | {'text/plain': 'source/main'}, 15 | 'blueSource', 16 | editor_factory=ADTObjectSourceEditor 17 | ) 18 | 19 | def __init__(self, connection, name, package=None, metadata=None): 20 | super().__init__(connection, name, metadata, active_status='inactive') 21 | 22 | self._metadata.package_reference.name = package 23 | -------------------------------------------------------------------------------- /sap/adt/table.py: -------------------------------------------------------------------------------- 1 | """ABAP Table ADT functionality module""" 2 | 3 | from sap.adt.objects import ADTObject, ADTObjectType, xmlns_adtcore_ancestor, ADTObjectSourceEditor 4 | 5 | 6 | class Table(ADTObject): 7 | """ABAP Table""" 8 | 9 | OBJTYPE = ADTObjectType( 10 | 'TABL/DT', 11 | 'ddic/tables', 12 | xmlns_adtcore_ancestor('blue', 'http://www.sap.com/wbobj/blue'), 13 | 'application/vnd.sap.adt.tables.v2+xml', 14 | {'text/plain': 'source/main'}, 15 | 'blueSource', 16 | editor_factory=ADTObjectSourceEditor 17 | ) 18 | 19 | def __init__(self, connection, name, package=None, metadata=None): 20 | super().__init__(connection, name, metadata, active_status='inactive') 21 | 22 | self._metadata.package_reference.name = package 23 | -------------------------------------------------------------------------------- /sap/cli/abapclass.py: -------------------------------------------------------------------------------- 1 | """ADT proxy for ABAP Class (OO)""" 2 | 3 | import sap.adt 4 | import sap.adt.wb 5 | import sap.cli.core 6 | import sap.cli.object 7 | 8 | 9 | SOURCE_TYPES = ['main', 'definitions', 'implementations', 'testclasses'] 10 | 11 | FILE_NAME_SUFFIX_TYPES = { 12 | 'clas.abap': None, 13 | 'clas.locals_def.abap': 'definitions', 14 | 'clas.testclasses.abap': 'testclasses', 15 | 'clas.locals_imp.abap': 'implementations' 16 | } 17 | 18 | 19 | def instance_from_args(connection, name, typ, args, metadata): 20 | """Converts command line arguments to an instance of an ADT class 21 | based on the given typ. 22 | """ 23 | 24 | package = None 25 | if hasattr(args, 'package'): 26 | package = args.package 27 | 28 | clas = sap.adt.Class(connection, name.upper(), package=package, metadata=metadata) 29 | 30 | if typ == 'definitions': 31 | return clas.definitions 32 | 33 | if typ == 'implementations': 34 | return clas.implementations 35 | 36 | if typ == 'testclasses': 37 | return clas.test_classes 38 | 39 | return clas 40 | 41 | 42 | class CommandGroup(sap.cli.object.CommandGroupObjectMaster): 43 | """Commands for Class""" 44 | 45 | def __init__(self): 46 | super().__init__('class') 47 | 48 | self.define() 49 | 50 | def instance(self, connection, name, args, metadata=None): 51 | typ = None 52 | if hasattr(args, 'type'): 53 | typ = args.type 54 | 55 | return instance_from_args(connection, name, typ, args, metadata) 56 | 57 | def instance_from_file_path(self, connection, filepath, args, metadata=None): 58 | name, suffix = sap.cli.object.object_name_from_source_file(filepath) 59 | 60 | try: 61 | typ = FILE_NAME_SUFFIX_TYPES[suffix] 62 | except KeyError as ex: 63 | raise sap.cli.core.InvalidCommandLineError(f'Unknown class file name suffix: "{suffix}"') from ex 64 | 65 | return instance_from_args(connection, name, typ, args, metadata) 66 | 67 | def define_read(self, commands): 68 | read_cmd = super().define_read(commands) 69 | read_cmd.insert_argument(1, '--type', default=SOURCE_TYPES[0], choices=SOURCE_TYPES) 70 | 71 | return read_cmd 72 | 73 | def define_write(self, commands): 74 | write_cmd = super().define_write(commands) 75 | write_cmd.insert_argument(1, '--type', default=SOURCE_TYPES[0], choices=SOURCE_TYPES) 76 | 77 | return write_cmd 78 | 79 | 80 | @CommandGroup.argument('name') 81 | @CommandGroup.command() 82 | def attributes(connection, args): 83 | """Prints out some attributes of the given class. 84 | """ 85 | 86 | clas = sap.adt.Class(connection, args.name.upper()) 87 | clas.fetch() 88 | 89 | print(f'Name : {clas.name}') 90 | print(f'Description: {clas.description}') 91 | print(f'Responsible: {clas.responsible}') 92 | # pylint: disable=no-member 93 | print(f'Package : {clas.reference.name}') 94 | 95 | 96 | @CommandGroup.argument('name') 97 | @CommandGroup.command() 98 | def execute(connection, args): 99 | """Runs the if_oo_adt_classrun~main method""" 100 | 101 | clas = sap.adt.Class(connection, args.name.upper()) 102 | print(clas.execute()) 103 | -------------------------------------------------------------------------------- /sap/cli/abapgit.py: -------------------------------------------------------------------------------- 1 | """ 2 | ADT proxy for ababgit commands 3 | """ 4 | 5 | import time 6 | 7 | import sap.adt.abapgit 8 | import sap.cli.core 9 | import sap.cli.helpers 10 | 11 | 12 | class CommandGroup(sap.cli.core.CommandGroup): 13 | """Commands for ababgit""" 14 | 15 | def __init__(self): 16 | super().__init__('abapgit') 17 | 18 | 19 | @CommandGroup.argument('--corrnr', type=str, nargs='?') 20 | @CommandGroup.argument('--remote-user', type=str, nargs='?') 21 | @CommandGroup.argument('--remote-password', type=str, nargs='?') 22 | @CommandGroup.argument('--branch', type=str, nargs='?', default='refs/heads/master') 23 | @CommandGroup.argument('url') 24 | @CommandGroup.argument('package') 25 | @CommandGroup.command() 26 | def link(connection, args): 27 | """ link git repository to ABAP package 28 | """ 29 | 30 | resp = sap.adt.abapgit.Repository.link(connection, { 31 | 'package': args.package.upper(), 32 | 'url': args.url, 33 | 'branchName': args.branch, 34 | 'remoteUser': args.remote_user, 35 | 'remotePassword': args.remote_password, 36 | 'transportRequest': args.corrnr 37 | }) 38 | 39 | console = sap.cli.core.get_console() 40 | if resp.status_code == 200: 41 | console.printout('Repository was linked.') 42 | else: 43 | console.printerr(f'Failed to link repository: {args.package}', resp) 44 | 45 | 46 | @ CommandGroup.argument('--corrnr', type=str, nargs='?') 47 | @ CommandGroup.argument('--branch', type=str, nargs='?', default=None) 48 | @ CommandGroup.argument('--remote-user', type=str, nargs='?') 49 | @ CommandGroup.argument('--remote-password', type=str, nargs='?') 50 | @ CommandGroup.argument('package') 51 | @ CommandGroup.command() 52 | def pull(connection, args): 53 | """ pull git repository branch to linked ABAP package 54 | """ 55 | 56 | repository = sap.adt.abapgit.Repository(connection, args.package.upper()) 57 | repository.fetch() 58 | repository.pull({ 59 | 'branchName': args.branch, 60 | 'remoteUser': args.remote_user, 61 | 'remotePassword': args.remote_password, 62 | 'transportRequest': args.corrnr 63 | }) 64 | 65 | repository.fetch() 66 | console = sap.cli.core.get_console() 67 | with sap.cli.helpers.ConsoleHeartBeat(console, 1): 68 | while repository.get_status() == 'R': 69 | time.sleep(1) 70 | repository.fetch() 71 | 72 | if repository.get_status() == 'E' or repository.get_status() == 'A': 73 | console.printerr(repository.get_status_text()) 74 | console.printerr(repository.get_error_log()) 75 | else: 76 | console.printout(repository.get_status_text()) 77 | -------------------------------------------------------------------------------- /sap/cli/activation.py: -------------------------------------------------------------------------------- 1 | """ADT proxy for Object Activation routines""" 2 | 3 | import sap 4 | import sap.cli.core 5 | from sap.adt.wb import fetch_inactive_objects 6 | from sap.cli.core import printout 7 | 8 | 9 | class InactiveObjectsGroup(sap.cli.core.CommandGroup): 10 | """Container for inactive objects commands.""" 11 | 12 | def __init__(self): 13 | super().__init__('inactiveobjects') 14 | 15 | 16 | class CommandGroup(sap.cli.core.CommandGroup): 17 | """Adapter converting command line parameters to sap.adt.wb.* 18 | methods calls. 19 | """ 20 | 21 | def __init__(self): 22 | super().__init__('activation') 23 | 24 | self.inactive_objects_grp = InactiveObjectsGroup() 25 | 26 | def install_parser(self, arg_parser): 27 | activation_group = super().install_parser(arg_parser) 28 | 29 | inobj_parser = activation_group.add_parser(self.inactive_objects_grp.name) 30 | 31 | self.inactive_objects_grp.install_parser(inobj_parser) 32 | 33 | 34 | @InactiveObjectsGroup.command('list') 35 | # pylint: disable=unused-argument 36 | def inactiveobjects_list(connection, args): 37 | """Print out all inactive objects""" 38 | 39 | def print_entry(entry, prefix=''): 40 | printout(f'{prefix}{entry.object.name} ({entry.object.typ})') 41 | 42 | inactive_results = fetch_inactive_objects(connection) 43 | 44 | handled = set() 45 | 46 | for root_entry in inactive_results.entries: 47 | if root_entry.object.parent_uri: 48 | continue 49 | 50 | root_entry_uri = root_entry.object.uri 51 | if root_entry_uri in handled: 52 | continue 53 | 54 | print_entry(root_entry) 55 | 56 | handled.add(root_entry_uri) 57 | 58 | for child_entry in inactive_results.entries: 59 | if root_entry_uri != child_entry.object.parent_uri: 60 | continue 61 | 62 | child_uri = child_entry.object.uri 63 | if child_uri in handled: 64 | continue 65 | 66 | handled.add(child_uri) 67 | print_entry(child_entry, prefix=' + ') 68 | 69 | for leftover_entry in inactive_results.entries: 70 | leftover_uri = leftover_entry.object.uri 71 | if leftover_uri in handled: 72 | continue 73 | 74 | print_entry(leftover_entry) 75 | -------------------------------------------------------------------------------- /sap/cli/adt.py: -------------------------------------------------------------------------------- 1 | """ADT configuration and parameters""" 2 | 3 | import sap.adt 4 | import sap.cli.core 5 | 6 | 7 | class CommandGroup(sap.cli.core.CommandGroup): 8 | """Commands for discovering ADT configuration""" 9 | 10 | def __init__(self): 11 | super().__init__('adt') 12 | 13 | 14 | @CommandGroup.command('collections') 15 | def abapclass(connection, _): 16 | """List object type and supported ADT XML format versions""" 17 | 18 | console = sap.cli.core.get_console() 19 | 20 | for typ, versions in connection.collection_types.items(): 21 | console.printout(typ) 22 | 23 | for ver in versions: 24 | console.printout(' ', ver) 25 | -------------------------------------------------------------------------------- /sap/cli/badi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ADT proxy for New BAdI (Enhancement Implementation BAdIs) commands 3 | """ 4 | 5 | import sap 6 | import sap.adt 7 | import sap.cli.core 8 | import sap.cli.object 9 | 10 | 11 | def mod_log(): 12 | """ADT Module logger""" 13 | 14 | return sap.get_logger() 15 | 16 | 17 | def _get_enhancement_implementation(connection, args): 18 | enho = sap.adt.EnhancementImplementation(connection, args.enhancement_implementation) 19 | enho.fetch() 20 | return enho 21 | 22 | 23 | def _list(connection, args): 24 | console = sap.cli.core.get_console() 25 | 26 | enho = _get_enhancement_implementation(connection, args) 27 | 28 | for badi in enho.specific.badis.implementations: 29 | console.printout(badi.name, badi.active, badi.implementing_class.name, badi.badi_definition.name, 30 | badi.customizing_lock, badi.default, badi.example, badi.short_text) 31 | 32 | 33 | class CommandGroup(sap.cli.core.CommandGroup): 34 | """Commands for BAdI""" 35 | 36 | def __init__(self): 37 | super().__init__('badi') 38 | 39 | def install_parser(self, arg_parser): 40 | super().install_parser(arg_parser) 41 | 42 | arg_parser.set_defaults(execute=_list) 43 | arg_parser.add_argument('-i', '--enhancement_implementation', help='BAdI Enhancement Implementation Name') 44 | 45 | 46 | @CommandGroup.command('list') 47 | def list_badis(connection, args): 48 | """List BAdIs for the given Enhancement Implementation name" 49 | """ 50 | 51 | return _list(connection, args) 52 | 53 | 54 | @CommandGroup.argument('-a', '--activate', action='store_true', default=False, help='Activate after modification') 55 | @CommandGroup.argument('--corrnr', nargs='?', default=None, help='transport number') 56 | @CommandGroup.argument('-b', '--badi', required=True, help='BAdI implementation name') 57 | @CommandGroup.argument('active', choices=['true', 'false'], help='New value for Active') 58 | @CommandGroup.command('set-active') 59 | def set_active(connection, args): 60 | """Modify the active state 61 | """ 62 | 63 | try: 64 | value = {'true': True, 'false': False}[args.active] 65 | except KeyError as exc: 66 | raise sap.errors.SAPCliError(f'BUG: unexpected value of the argument active: {args.active}') from exc 67 | 68 | console = sap.cli.core.get_console() 69 | 70 | enho = _get_enhancement_implementation(connection, args) 71 | try: 72 | badi = enho.specific.badis.implementations[args.badi] 73 | except KeyError as exc: 74 | msg = f'The BAdI {args.badi} not found in the enhancement implementation {args.enhancement_implementation}' 75 | raise sap.errors.SAPCliError(msg) from exc 76 | 77 | if badi.is_active_implementation == value: 78 | console.printout(f'Nothing to do! The BAdI {args.badi}\'s Active is already: {args.active}') 79 | return 0 80 | 81 | badi.is_active_implementation = value 82 | 83 | console.printout('Updating:') 84 | console.printout(f'* {args.enhancement_implementation}/{args.badi}') 85 | with enho.open_editor(corrnr=args.corrnr) as editor: 86 | editor.push() 87 | 88 | if not args.activate: 89 | return 0 90 | 91 | activator = sap.cli.wb.ObjectActivationWorker() 92 | sap.cli.object.activate_object_list(activator, ((args.enhancement_implementation, enho),), count=1) 93 | return 0 94 | -------------------------------------------------------------------------------- /sap/cli/datadefinition.py: -------------------------------------------------------------------------------- 1 | """ADT proxy for Data Definition (CDS)""" 2 | 3 | import sap.adt 4 | import sap.adt.wb 5 | import sap.cli.core 6 | import sap.cli.object 7 | 8 | 9 | class CommandGroup(sap.cli.core.CommandGroup): 10 | """Adapter converting command line parameters to sap.adt.DataDefinition 11 | methods calls. 12 | """ 13 | 14 | def __init__(self): 15 | super().__init__('ddl') 16 | 17 | 18 | @CommandGroup.argument('name') 19 | @CommandGroup.command() 20 | def read(connection, args): 21 | """Prints it out based on command line configuration. 22 | """ 23 | 24 | ddl = sap.adt.DataDefinition(connection, args.name) 25 | print(ddl.text) 26 | 27 | 28 | @CommandGroup.argument('name', nargs='+') 29 | @CommandGroup.command() 30 | def activate(connection, args): 31 | """Actives the given class. 32 | """ 33 | 34 | activator = sap.cli.wb.ObjectActivationWorker() 35 | activated_items = ((name, sap.adt.DataDefinition(connection, name)) for name in args.name) 36 | sap.cli.object.activate_object_list(activator, activated_items, count=len(args.name)) 37 | -------------------------------------------------------------------------------- /sap/cli/datapreview.py: -------------------------------------------------------------------------------- 1 | """ADT SQL Console Functions""" 2 | 3 | import json 4 | 5 | import sap.adt 6 | from sap.cli.core import printout 7 | import sap.cli.core 8 | 9 | 10 | class CommandGroup(sap.cli.core.CommandGroup): 11 | """Adapter converting command line parameters to sap.adt.DataPreview methods 12 | calls. 13 | """ 14 | 15 | def __init__(self): 16 | super().__init__('datapreview') 17 | 18 | 19 | @CommandGroup.argument('-n', '--noheadings', action='store_true', default=False) 20 | @CommandGroup.argument('-o', '--output', choices=['human', 'json'], default='human') 21 | @CommandGroup.argument('--noaging', action='store_false', default=True) 22 | @CommandGroup.argument('--rows', type=int, default=100) 23 | @CommandGroup.argument('statement', type=str, help='ABAP SQL syntax without period') 24 | @CommandGroup.command() 25 | def osql(connection, args): 26 | """Executes OpenSQL query""" 27 | 28 | sqlconsole = sap.adt.DataPreview(connection) 29 | table = sqlconsole.execute(args.statement, rows=args.rows, aging=args.noaging) 30 | 31 | if args.output == 'json': 32 | printout(json.dumps(table, indent=2)) 33 | else: 34 | header = args.noheadings 35 | for row in table: 36 | if not header: 37 | printout(' | '.join(row.keys())) 38 | header = True 39 | 40 | printout(' | '.join(row.values())) 41 | -------------------------------------------------------------------------------- /sap/cli/flp.py: -------------------------------------------------------------------------------- 1 | """flp methods""" 2 | 3 | import sap.cli.core 4 | import sap.flp 5 | 6 | 7 | class CommandGroup(sap.cli.core.CommandGroup): 8 | """Management for FLP Catalog""" 9 | 10 | def __init__(self): 11 | super().__init__('flp') 12 | 13 | 14 | @CommandGroup.argument('--config', type=str, required=True, help="Configuration file path") 15 | @CommandGroup.command() 16 | def init(connection, args): 17 | """Initializes the Fiori Launchpad 18 | """ 19 | 20 | builder = sap.flp.builder.Builder(connection, args.config) 21 | builder.run() 22 | -------------------------------------------------------------------------------- /sap/cli/include.py: -------------------------------------------------------------------------------- 1 | """ADT proxy for ABAP Program Include""" 2 | 3 | import sap.adt 4 | import sap.cli.object 5 | import sap.cli.core 6 | 7 | 8 | class CommandGroup(sap.cli.object.CommandGroupObjectMaster): 9 | """Adapter converting command line parameters to sap.adt.Include methods 10 | calls. 11 | """ 12 | 13 | def __init__(self): 14 | super().__init__('include') 15 | 16 | self.define() 17 | 18 | def instance(self, connection, name, args, metadata=None): 19 | master, package = None, None 20 | 21 | if hasattr(args, 'package'): 22 | package = args.package 23 | 24 | if hasattr(args, 'master'): 25 | master = args.master 26 | 27 | return sap.adt.Include(connection, name.upper(), master=master, package=package, metadata=metadata) 28 | 29 | def define_activate(self, commands): 30 | activate_cmd = super().define_activate(commands) 31 | activate_cmd.append_argument('-m', '--master', nargs='?', default=None, help='Master program') 32 | 33 | return activate_cmd 34 | 35 | 36 | @CommandGroup.argument('name') 37 | @CommandGroup.command() 38 | def attributes(connection, args): 39 | """Prints out some attributes of the given include. 40 | """ 41 | 42 | proginc = sap.adt.Include(connection, args.name.upper()) 43 | proginc.fetch() 44 | 45 | console = sap.cli.core.get_console() 46 | 47 | console.printout(f'Name : {proginc.name}') 48 | console.printout(f'Description: {proginc.description}') 49 | console.printout(f'Responsible: {proginc.responsible}') 50 | # pylint: disable=no-member 51 | console.printout(f'Package : {proginc.reference.name}') 52 | 53 | context = proginc.context 54 | if context is not None: 55 | console.printout(f'Main : {context.name} ({context.typ})') 56 | else: 57 | console.printout('Main :') 58 | -------------------------------------------------------------------------------- /sap/cli/interface.py: -------------------------------------------------------------------------------- 1 | """ADT proxy for ABAP Interface (OO)""" 2 | 3 | import sap.adt 4 | import sap.cli.object 5 | 6 | 7 | class CommandGroup(sap.cli.object.CommandGroupObjectMaster): 8 | """Adapter converting command line parameters to sap.adt.Interface methods 9 | calls. 10 | """ 11 | 12 | def __init__(self): 13 | super().__init__('interface') 14 | 15 | self.define() 16 | 17 | def instance(self, connection, name, args, metadata=None): 18 | package = None 19 | if hasattr(args, 'package'): 20 | package = args.package 21 | 22 | return sap.adt.Interface(connection, name.upper(), package=package, metadata=metadata) 23 | -------------------------------------------------------------------------------- /sap/cli/program.py: -------------------------------------------------------------------------------- 1 | """ADT proxy for ABAP Program (Report)""" 2 | 3 | import sap.adt 4 | import sap.cli.object 5 | 6 | 7 | class CommandGroup(sap.cli.object.CommandGroupObjectMaster): 8 | """Adapter converting command line parameters to sap.adt.Program methods 9 | calls. 10 | """ 11 | 12 | def __init__(self): 13 | super().__init__('program') 14 | 15 | self.define() 16 | 17 | def instance(self, connection, name, args, metadata=None): 18 | package = None 19 | if hasattr(args, 'package'): 20 | package = args.package 21 | 22 | return sap.adt.Program(connection, name.upper(), package=package, metadata=metadata) 23 | -------------------------------------------------------------------------------- /sap/cli/rap.py: -------------------------------------------------------------------------------- 1 | """ 2 | ADT proxy for service binding commands 3 | """ 4 | 5 | import sap.adt 6 | import sap.cli.core 7 | import sap.cli.helpers 8 | import sap.cli.wb 9 | import sap.cli.object 10 | 11 | 12 | class DefinitionGroup(sap.cli.core.CommandGroup): 13 | """Container for definition commands.""" 14 | 15 | def __init__(self): 16 | super().__init__('definition') 17 | 18 | 19 | class BindingGroup(sap.cli.core.CommandGroup): 20 | """Container for binding commands.""" 21 | 22 | def __init__(self): 23 | super().__init__('binding') 24 | 25 | 26 | class CommandGroup(sap.cli.core.CommandGroup): 27 | """Commands for rap """ 28 | 29 | def __init__(self): 30 | super().__init__('rap') 31 | 32 | self.definition_grp = DefinitionGroup() 33 | self.binding_grp = BindingGroup() 34 | 35 | def install_parser(self, arg_parser): 36 | activation_group = super().install_parser(arg_parser) 37 | 38 | binding_parser = activation_group.add_parser(self.binding_grp.name) 39 | self.binding_grp.install_parser(binding_parser) 40 | 41 | definition_parser = activation_group.add_parser(self.definition_grp.name) 42 | self.definition_grp.install_parser(definition_parser) 43 | 44 | 45 | @BindingGroup.argument('--service', nargs='?', default=None, 46 | help='Service name of the binding\'s services to publish') 47 | @BindingGroup.argument('--version', nargs='?', default=None, 48 | help='Version of the binding\'s services to publish') 49 | @BindingGroup.argument('binding_name') 50 | @BindingGroup.command() 51 | def publish(connection, args): 52 | """ publish odata service that belongs to a service binding identified by a version 53 | """ 54 | 55 | console = sap.cli.core.get_console() 56 | 57 | binding = sap.adt.businessservice.ServiceBinding(connection, args.binding_name) 58 | binding.fetch() 59 | 60 | if not binding.services: 61 | console.printerr( 62 | f'Business Service Biding {args.binding_name} does not contain any services') 63 | return 1 64 | 65 | if args.service is None and args.version is None: 66 | if len(binding.services) > 1: 67 | console.printerr( 68 | f'''Cannot publish Business Service Biding {args.binding_name} without 69 | Service Definition filters because the business binding contains more than one 70 | Service Definition''') 71 | return 1 72 | 73 | service = binding.services[0] 74 | else: 75 | service = binding.find_service(args.service, args.version) 76 | if service is None: 77 | console.printerr( 78 | f'''Business Service Binding {args.binding_name} has no Service Definition 79 | with supplied name "{args.service or ''}" and version "{args.version or ''}"''') 80 | return 1 81 | 82 | status = binding.publish(service) 83 | 84 | console.printout(status.SHORT_TEXT) 85 | if status.LONG_TEXT: 86 | console.printout(status.LONG_TEXT) 87 | 88 | if status.SEVERITY != "OK": 89 | console.printerr(f'Failed to publish Service {service.definition.name} in Binding {args.binding_name}') 90 | return 1 91 | 92 | console.printout( 93 | f'Service {service.definition.name} in Binding {args.binding_name} published successfully.') 94 | return 0 95 | 96 | 97 | @DefinitionGroup.argument('name', nargs='+') 98 | @DefinitionGroup.command('activate') 99 | def definition_activate(connection, args): 100 | """Activate Business Service Definition""" 101 | 102 | activator = sap.cli.wb.ObjectActivationWorker() 103 | activated_items = ((name, sap.adt.ServiceDefinition(connection, name)) for name in args.name) 104 | return sap.cli.object.activate_object_list(activator, activated_items, count=len(args.name)) 105 | -------------------------------------------------------------------------------- /sap/cli/structure.py: -------------------------------------------------------------------------------- 1 | """Operations on top of ABAP Structure""" 2 | 3 | import sap.adt 4 | import sap.cli.object 5 | 6 | 7 | class CommandGroup(sap.cli.object.CommandGroupObjectMaster): 8 | """Adapter converting command line parameters to sap.adt.Structure methods 9 | calls. 10 | """ 11 | 12 | def __init__(self): 13 | super().__init__('structure') 14 | 15 | self.define() 16 | 17 | def instance(self, connection, name, args, metadata=None): 18 | package = getattr(args, 'package', None) 19 | 20 | return sap.adt.Structure(connection, name, package, metadata) 21 | -------------------------------------------------------------------------------- /sap/cli/table.py: -------------------------------------------------------------------------------- 1 | """ADT Proxy for ABAP Table""" 2 | 3 | import sap.adt 4 | import sap.cli.object 5 | 6 | 7 | class CommandGroup(sap.cli.object.CommandGroupObjectMaster): 8 | """Adapter converting command line parameters to sap.adt.Table methods 9 | calls. 10 | """ 11 | 12 | def __init__(self): 13 | super().__init__('table') 14 | 15 | self.define() 16 | 17 | def instance(self, connection, name, args, metadata=None): 18 | package = getattr(args, 'package', None) 19 | 20 | return sap.adt.Table(connection, name, package, metadata) 21 | -------------------------------------------------------------------------------- /sap/cli/user.py: -------------------------------------------------------------------------------- 1 | """ABAP User handling methods""" 2 | 3 | import sap.cli.core 4 | import sap.cli.helpers 5 | import sap.rfc.user 6 | 7 | 8 | class CommandGroup(sap.cli.core.CommandGroup): 9 | """Adapter converting command line parameters to sap.rfc.user 10 | methods calls. 11 | """ 12 | 13 | def __init__(self): 14 | super().__init__('user') 15 | 16 | 17 | @CommandGroup.argument('username') 18 | @CommandGroup.command() 19 | def details(connection, args): 20 | """Dump user details""" 21 | 22 | manager = sap.rfc.user.UserManager() 23 | rfc_details = manager.fetch_user_details(connection, args.username) 24 | sap.cli.core.printout('User :', args.username) 25 | sap.cli.core.printout('Alias :', rfc_details['ALIAS']['USERALIAS']) 26 | sap.cli.core.printout('Last Login:', rfc_details['LOGONDATA']['LTIME']) 27 | 28 | 29 | @CommandGroup.argument('--type', choices=['Dialog', 'Service', 'System'], default='Dialog') 30 | @CommandGroup.argument('--new-password', nargs='?') 31 | @CommandGroup.argument('username') 32 | @CommandGroup.command() 33 | def create(connection, args): 34 | """Dump user details""" 35 | 36 | manager = sap.rfc.user.UserManager() 37 | 38 | builder = manager.user_builder() 39 | builder.set_username(args.username) 40 | builder.set_type(args.type) 41 | builder.set_password(args.new_password) 42 | 43 | sap.cli.core.printout(manager.create_user(connection, builder)) 44 | 45 | 46 | @CommandGroup.argument('--new-password', nargs='?') 47 | @CommandGroup.argument('username') 48 | @CommandGroup.command() 49 | def change(connection, args): 50 | """Dump user details""" 51 | 52 | manager = sap.rfc.user.UserManager() 53 | 54 | builder = manager.user_builder() 55 | builder.set_username(args.username) 56 | builder.set_password(args.new_password) 57 | 58 | sap.cli.core.printout(manager.change_user(connection, builder)) 59 | -------------------------------------------------------------------------------- /sap/config.py: -------------------------------------------------------------------------------- 1 | """Configuration""" 2 | 3 | import os 4 | from typing import Any 5 | 6 | 7 | def config_get(option: str, default: Any = None) -> Any: 8 | """Returns configuration values""" 9 | 10 | config = {'http_timeout': float(os.environ.get('SAPCLI_HTTP_TIMEOUT', 900))} 11 | 12 | return config.get(option, default) 13 | -------------------------------------------------------------------------------- /sap/errors.py: -------------------------------------------------------------------------------- 1 | """SAP CLI error types""" 2 | 3 | 4 | class FatalError(Exception): 5 | """Common base exception type for """ 6 | 7 | # pylint: disable=unnecessary-pass 8 | pass 9 | 10 | 11 | class SAPCliError(FatalError): 12 | """Common base exception type for runtime errors""" 13 | 14 | # pylint: disable=unnecessary-pass 15 | pass 16 | 17 | 18 | class ResourceAlreadyExistsError(SAPCliError): 19 | """Exception for existing resources - e.g. item to be created""" 20 | 21 | # pylint: disable=unnecessary-pass 22 | pass 23 | 24 | def __repr__(self): 25 | return 'Resource already exists' 26 | 27 | def __str__(self): 28 | return repr(self) 29 | 30 | 31 | class InputError(FatalError): 32 | """Common base exception type for runtime input errors 33 | usually caused by users. 34 | """ 35 | 36 | # pylint: disable=unnecessary-pass 37 | pass 38 | -------------------------------------------------------------------------------- /sap/flp/__init__.py: -------------------------------------------------------------------------------- 1 | """Fiori Launchpad wrappers and helpers""" 2 | 3 | from sap.flp.builder import Builder # noqa: F401 4 | from sap.flp.service import Service # noqa: F401 5 | -------------------------------------------------------------------------------- /sap/odata/__init__.py: -------------------------------------------------------------------------------- 1 | """OData wrappers and helpers""" 2 | 3 | from sap.odata.connection import Connection # noqa: F401 4 | -------------------------------------------------------------------------------- /sap/odata/connection.py: -------------------------------------------------------------------------------- 1 | """OData connection helpers""" 2 | 3 | import pyodata 4 | import requests 5 | from requests.auth import HTTPBasicAuth 6 | 7 | from sap import get_logger, config_get 8 | from sap.odata.errors import HTTPRequestError, UnauthorizedError, TimedOutRequestError 9 | 10 | 11 | # pylint: disable=too-many-instance-attributes,too-few-public-methods 12 | class Connection: 13 | """OData communication built on top of pyodata and requests. 14 | """ 15 | 16 | client = None 17 | 18 | # pylint: disable=too-many-arguments 19 | def __init__(self, service, host, port, client, user, password, ssl, verify): 20 | """Parameters: 21 | - service: id of the odata service (e.g. UI5/ABAP_REPOSITORY_SRV) 22 | - host: string host name or IP of 23 | - port: string TCP/IP port for abap application server 24 | - client: string SAP client 25 | - user: string user name 26 | - password: string user password 27 | - ssl: boolean to switch between http and https 28 | - verify: boolean to switch SSL validation on/off 29 | """ 30 | 31 | if ssl: 32 | protocol = 'https' 33 | if port is None: 34 | port = '443' 35 | else: 36 | protocol = 'http' 37 | if port is None: 38 | port = '80' 39 | 40 | self._base_url = f'{protocol}://{host}:{port}/sap/opu/odata/{service}' 41 | self._query_args = f'sap-client={client}&saml2=disabled' 42 | self._user = user 43 | self._auth = HTTPBasicAuth(user, password) 44 | self._timeout = config_get('http_timeout') 45 | 46 | self._session = requests.Session() 47 | self._session.verify = verify 48 | self._session.auth = (user, password) 49 | 50 | # csrf token handling for all future "create" requests 51 | try: 52 | get_logger().info('Executing head request as part of CSRF authentication %s', self._base_url) 53 | req = requests.Request('HEAD', self._base_url, headers={'x-csrf-token': 'fetch'}) 54 | req = self._session.prepare_request(req) 55 | res = self._session.send(req, timeout=self._timeout) 56 | 57 | except requests.exceptions.ConnectTimeout as ex: 58 | raise TimedOutRequestError(req, self._timeout) from ex 59 | 60 | if res.status_code == 401: 61 | raise UnauthorizedError(req, res, self._user) 62 | 63 | if res.status_code >= 400: 64 | raise HTTPRequestError(req, res) 65 | 66 | token = res.headers.get('x-csrf-token', '') 67 | self._session.headers.update({'x-csrf-token': token}) 68 | 69 | # instance of the service 70 | self.client = pyodata.Client(self._base_url, self._session) 71 | -------------------------------------------------------------------------------- /sap/odata/errors.py: -------------------------------------------------------------------------------- 1 | """HTTP related errors""" 2 | 3 | from sap.errors import SAPCliError 4 | 5 | 6 | class HTTPRequestError(SAPCliError): 7 | """Exception for unexpected HTTP responses""" 8 | 9 | def __init__(self, request, response): 10 | super().__init__() 11 | 12 | self.request = request 13 | self.response = response 14 | 15 | def __repr__(self): 16 | return f'{self.response.status_code}\n{self.response.text}' 17 | 18 | def __str__(self): 19 | return repr(self) 20 | 21 | 22 | class UnauthorizedError(SAPCliError): 23 | """Exception for unauthorized """ 24 | 25 | def __init__(self, request, response, user): 26 | super().__init__() 27 | 28 | self.request = request 29 | self.response = response 30 | self.method = request.method 31 | self.url = request.url 32 | self.user = user 33 | 34 | def __repr__(self): 35 | return f'Authorization for the user "{self.user}" has failed: {self.method} {self.url}' 36 | 37 | def __str__(self): 38 | return repr(self) 39 | 40 | 41 | class TimedOutRequestError(SAPCliError): 42 | """Exception for timeout requests""" 43 | 44 | def __init__(self, request, timeout): 45 | super().__init__() 46 | 47 | self.request = request 48 | self.method = request.method 49 | self.url = request.url 50 | self.timeout = timeout 51 | 52 | def __repr__(self): 53 | return f'The request {self.method} {self.url} took more than {self.timeout}s' 54 | 55 | def __str__(self): 56 | return repr(self) 57 | -------------------------------------------------------------------------------- /sap/platform/__init__.py: -------------------------------------------------------------------------------- 1 | """SAP as a Platform""" 2 | -------------------------------------------------------------------------------- /sap/platform/language.py: -------------------------------------------------------------------------------- 1 | """ABAP Platform helpers and utilities""" 2 | 3 | from locale import getlocale 4 | 5 | from sap.errors import SAPCliError 6 | 7 | # Supported Languages and Code Pages (Non-Unicode) 8 | # https://help.sap.com/viewer/f3941a838b254ba396a9d92d9dea7294/7.4.19/en-US/c1ae563cd2ad4f0ce10000000a11402f.html 9 | 10 | CODE_LIST = [ 11 | ('AF', 'a'), # Afrikaans 12 | ('AR', 'A'), # Arabic 13 | ('BG', 'W'), # Bulgarian 14 | ('CA', 'c'), # Catalan 15 | ('ZH', '1'), # Chinese 16 | ('ZF', 'M'), # Chinese-traditional 17 | ('HR', '6'), # Croatian 18 | ('CS', 'C'), # Czech 19 | ('DA', 'K'), # Danish 20 | ('NL', 'N'), # Dutch 21 | ('EN', 'E'), # English 22 | ('ET', '9'), # Estonian 23 | ('FI', 'U'), # Finnish 24 | ('FR', 'F'), # French 25 | ('DE', 'D'), # German 26 | ('EL', 'G'), # Greek 27 | ('HE', 'B'), # Hebrew 28 | ('HU', 'H'), # Hungarian 29 | ('IS', 'b'), # Icelandic 30 | ('ID', 'i'), # Indonesian 31 | ('IT', 'I'), # Italian 32 | ('JA', 'J'), # Japanese 33 | ('KO', '3'), # Korean 34 | ('LV', 'Y'), # Latvian 35 | ('LT', 'X'), # Lithuanian 36 | ('MS', '7'), # Malay 37 | ('NO', 'O'), # Norwegian 38 | ('PL', 'L'), # Polish 39 | ('PT', 'P'), # Portuguese 40 | ('Z1', 'Z'), # Reserved-custt. 41 | ('RO', '4'), # Romanian 42 | ('RU', 'R'), # Russian 43 | ('SR', '0'), # Serbian 44 | ('SH', 'd'), # Serbo-Croatian 45 | ('SK', 'Q'), # Slovakian 46 | ('SL', '5'), # Slovene 47 | ('ES', 'S'), # Spanish 48 | ('SV', 'V'), # Swedish 49 | ('TH', '2'), # Thai 50 | ('TR', 'T'), # Turkish 51 | ('UK', '8') # Ukrainian 52 | ] 53 | 54 | 55 | def sap_code_to_iso_code(sap_code: str) -> str: 56 | """Coverts one letter SAP language codes to ISO codes. 57 | 58 | Raises sap.errors.SAPCliError if the give sap_code is not identified. 59 | """ 60 | 61 | try: 62 | return next((entry[0] for entry in CODE_LIST if entry[1] == sap_code)) 63 | except StopIteration: 64 | # pylint: disable=raise-missing-from 65 | raise SAPCliError(f'Not found SAP Language Code: {sap_code}') 66 | 67 | 68 | def iso_code_to_sap_code(iso_code: str) -> str: 69 | """Coverts ISO codes to one letter SAP language codes""" 70 | 71 | iso_code = iso_code.upper() 72 | try: 73 | return next((entry[1] for entry in CODE_LIST if entry[0] == iso_code)) 74 | except StopIteration: 75 | # pylint: disable=raise-missing-from 76 | raise SAPCliError(f'Not found ISO Code: {iso_code}') 77 | 78 | 79 | def locale_lang_sap_code() -> str: 80 | """Reads current system locale and attempts to convert the language part 81 | to SAP Language Code 82 | """ 83 | 84 | loc = getlocale() 85 | lang = loc[0] or "" 86 | 87 | if len(lang) < 2: 88 | raise SAPCliError(f'The current system locale language is not ISO 3166: {lang}') 89 | 90 | try: 91 | return iso_code_to_sap_code(lang[0:2].upper()) 92 | except SAPCliError as ex: 93 | raise SAPCliError( 94 | f'The current system locale language cannot be converted to SAP language code: {lang}') from ex 95 | -------------------------------------------------------------------------------- /sap/rest/__init__.py: -------------------------------------------------------------------------------- 1 | """HTTP/REST wrappers and helpers""" 2 | 3 | from sap.rest.connection import Connection # noqa: F401 4 | -------------------------------------------------------------------------------- /sap/rest/errors.py: -------------------------------------------------------------------------------- 1 | """HTTP related errors""" 2 | 3 | import re 4 | from sap.errors import SAPCliError 5 | 6 | 7 | class UnexpectedResponseContent(SAPCliError): 8 | """Exception for unexpected responses content""" 9 | 10 | def __init__(self, expected, received, content): 11 | super().__init__() 12 | 13 | self.expected = expected 14 | self.received = received 15 | self.content = content 16 | 17 | def __str__(self): 18 | return f'Unexpected Content-Type: {self.received} with: {self.content}' 19 | 20 | 21 | class HTTPRequestError(SAPCliError): 22 | """Exception for unexpected HTTP responses""" 23 | 24 | def __init__(self, request, response): 25 | super().__init__() 26 | 27 | self.request = request 28 | self.response = response 29 | self.status_code = response.status_code 30 | 31 | def _get_error_header(self): 32 | return re.search('.*

*(.*) *

.*', self.response.text) 33 | 34 | def _get_error_message(self): 35 | error_msg = re.finditer('

*(.*?) *

', self.response.text, 36 | flags=re.DOTALL) 37 | error_msg = [msg[1] for msg in error_msg if 'Server time:' not in msg[1]] 38 | error_msg = [' '.join(msg.split('\n')) for msg in error_msg] 39 | error_msg = '\n'.join(error_msg) 40 | 41 | return error_msg 42 | 43 | def __repr__(self): 44 | error_text_header = self._get_error_header() 45 | error_msg = self._get_error_message() 46 | if error_text_header and error_msg: 47 | return f'{error_text_header[1]}\n{error_msg}' 48 | 49 | return f'{self.response.status_code}\n{self.response.text}' 50 | 51 | def __str__(self): 52 | return repr(self) 53 | 54 | 55 | class UnauthorizedError(SAPCliError): 56 | """Exception for unauthorized """ 57 | 58 | def __init__(self, request, response, user): 59 | super().__init__() 60 | 61 | self.request = request 62 | self.response = response 63 | self.status_code = response.status_code 64 | self.method = request.method 65 | self.url = request.url 66 | self.user = user 67 | 68 | def __repr__(self): 69 | return f'Authorization for the user "{self.user}" has failed: {self.method} {self.url}' 70 | 71 | def __str__(self): 72 | return repr(self) 73 | 74 | 75 | class TimedOutRequestError(SAPCliError): 76 | """Exception for timeout requests""" 77 | 78 | def __init__(self, request, timeout): 79 | super().__init__() 80 | 81 | self.request = request 82 | self.method = request.method 83 | self.url = request.url 84 | self.timeout = timeout 85 | 86 | def __repr__(self): 87 | return f'The request {self.method} {self.url} took more than {self.timeout}s' 88 | 89 | def __str__(self): 90 | return repr(self) 91 | 92 | 93 | class GCTSConnectionError(SAPCliError): 94 | """Exception for connection errors""" 95 | 96 | def __init__(self, host, port, ssl, message): 97 | super().__init__() 98 | msg = f'[HOST:"{host}", PORT:"{port}", SSL:"{ssl}"] Error: ' 99 | if 'Errno -5' in message: 100 | msg += 'Name resolution error. Check the HOST configuration.' 101 | elif 'Errno 111' in message: 102 | msg += 'Cannot connect to the system. Check the HOST and PORT configuration.' 103 | else: 104 | msg += message 105 | self.message = msg 106 | 107 | def __str__(self): 108 | return f'GCTS connection error: {self.message}' 109 | -------------------------------------------------------------------------------- /sap/rest/gcts/__init__.py: -------------------------------------------------------------------------------- 1 | """gCTS REST calls""" 2 | 3 | # ABAP Package: SCTS_ABAP_AND_VCS 4 | 5 | 6 | def package_name_from_url(url): 7 | """Parse out Package name from a repo git url""" 8 | 9 | url_repo_part = url.split('/')[-1] 10 | 11 | if url_repo_part.endswith('.git'): 12 | return url_repo_part[:-4] 13 | 14 | return url_repo_part 15 | -------------------------------------------------------------------------------- /sap/rest/gcts/errors.py: -------------------------------------------------------------------------------- 1 | """gCTS Error wrappers""" 2 | 3 | from sap.errors import SAPCliError 4 | 5 | 6 | class GCTSRequestError(SAPCliError): 7 | """Base gCTS error type""" 8 | 9 | def __init__(self, messages): 10 | super().__init__() 11 | 12 | self.messages = messages 13 | 14 | def __repr__(self): 15 | return f'gCTS exception: {self.messages["exception"]}' 16 | 17 | def __str__(self): 18 | return repr(self) 19 | 20 | 21 | class GCTSRepoAlreadyExistsError(GCTSRequestError): 22 | """A repository already exists""" 23 | 24 | # pylint: disable=unnecessary-pass 25 | pass 26 | 27 | 28 | class GCTSRepoNotExistsError(GCTSRequestError): 29 | """A repository does not exist""" 30 | 31 | def __init__(self, messages): 32 | super().__init__(messages) 33 | self.messages['exception'] = 'Repository does not exist' 34 | 35 | 36 | def exception_from_http_error(http_error): 37 | """Converts HTTPRequestError to proper instance""" 38 | 39 | if 'application/json' not in http_error.response.headers.get('Content-Type', ''): 40 | return http_error 41 | 42 | messages = http_error.response.json() 43 | 44 | log = messages.get('log', None) 45 | if log and log[0].get('message', '').endswith('Error action CREATE_REPOSITORY Repository already exists'): 46 | return GCTSRepoAlreadyExistsError(messages) 47 | 48 | exception = messages.get('exception', None) 49 | if exception == 'No relation between system and repository': 50 | return GCTSRepoNotExistsError(messages) 51 | 52 | return GCTSRequestError(messages) 53 | -------------------------------------------------------------------------------- /sap/rfc/__init__.py: -------------------------------------------------------------------------------- 1 | """PyRFC wrappers and utilities""" 2 | 3 | from sap.rfc.core import rfc_is_available, connect # noqa: F401 4 | -------------------------------------------------------------------------------- /sap/rfc/bapi.py: -------------------------------------------------------------------------------- 1 | """BAPI helpers and utilities""" 2 | 3 | from typing import Union, List 4 | 5 | from sap.errors import SAPCliError 6 | from sap.rfc.core import RFCResponse 7 | 8 | 9 | BAPIReturnRFC = Union[RFCResponse, List[RFCResponse]] 10 | 11 | BAPI_MTYPE = { 12 | 'S': 'Success', 13 | 'E': 'Error', 14 | 'W': 'Warning', 15 | 'I': 'Info', 16 | 'A': 'Abort' 17 | } 18 | 19 | 20 | def bapi_message_to_str(bapiret: RFCResponse): 21 | """Converts a single BAPI return value to a human readable string""" 22 | 23 | msg_type = bapiret['TYPE'] 24 | msg_id = bapiret['ID'] 25 | msg_number = bapiret['NUMBER'] 26 | msg_message = bapiret['MESSAGE'] 27 | 28 | msg_code = BAPI_MTYPE.get(msg_type, msg_type) 29 | if msg_id or (msg_number and msg_number != '000'): 30 | msg_code += f'({msg_id}|{msg_number})' 31 | 32 | return f'{msg_code}: {msg_message}' 33 | 34 | 35 | class BAPIReturn: 36 | """BAPI return value""" 37 | 38 | def __init__(self, bapi_return: BAPIReturnRFC): 39 | if isinstance(bapi_return, dict): 40 | self._bapirettab = [bapi_return] 41 | elif isinstance(bapi_return, list): 42 | self._bapirettab = bapi_return 43 | else: 44 | raise ValueError(f'Neither dict nor list BAPI return type: {type(bapi_return).__name__}') 45 | 46 | for bapiret in self._bapirettab: 47 | if bapiret['TYPE'] == 'E': 48 | self._error_message = bapi_message_to_str(bapiret) 49 | break 50 | else: 51 | self._error_message = None 52 | 53 | def __str__(self): 54 | return '\n'.join(self.message_lines()) 55 | 56 | def __getitem__(self, index): 57 | return self._bapirettab[index] 58 | 59 | def contains(self, msg_class, msg_number): 60 | """"Returns True if the list of messages contains specified Message 61 | Class and Message Number. 62 | """ 63 | 64 | return any((msg['ID'] == msg_class and msg['NUMBER'] == msg_number for msg in self._bapirettab)) 65 | 66 | def message_lines(self): 67 | """Returns list of strings with human readable messages. 68 | """ 69 | 70 | return [bapi_message_to_str(bapiret) for bapiret in self._bapirettab] 71 | 72 | @property 73 | def is_error(self): 74 | """Returns True if the BAPI response represents an error. 75 | Otherwise returns False. 76 | """ 77 | 78 | return self._error_message is not None 79 | 80 | @property 81 | def is_empty(self): 82 | """Returns True for BAPI response without any message.""" 83 | 84 | return len(self._bapirettab) == 0 85 | 86 | @property 87 | def error_message(self): 88 | """Returns the error message if there is any. If the BAPI return value 89 | does not represent an error, the property holds None. 90 | """ 91 | 92 | return self._error_message 93 | 94 | 95 | class BAPIError(SAPCliError): 96 | """RFC BAPI error""" 97 | 98 | def __init__(self, bapireturn: BAPIReturn, response): 99 | super().__init__(str(bapireturn)) 100 | 101 | self.bapiret = bapireturn 102 | self.response = response 103 | 104 | @staticmethod 105 | def raise_for_error(bapiret: BAPIReturnRFC, response) -> BAPIReturn: 106 | """If the given BAPI response contains error raise an exception""" 107 | 108 | bapi_return = BAPIReturn(bapiret) 109 | if bapi_return.is_error: 110 | raise BAPIError(bapi_return, response) 111 | 112 | return bapi_return 113 | -------------------------------------------------------------------------------- /sap/rfc/core.py: -------------------------------------------------------------------------------- 1 | """Base RFC functionality""" 2 | 3 | 4 | from typing import Any, Dict 5 | 6 | import sap 7 | import sap.errors 8 | 9 | from sap.rfc.errors import RFCLoginError, RFCCommunicationError 10 | 11 | 12 | RFCParams = Dict[str, Any] 13 | RFCResponse = Dict[str, Any] 14 | 15 | 16 | SAPRFC_MODULE = None 17 | 18 | 19 | def mod_log(): 20 | """rfc.core module logger""" 21 | 22 | return sap.get_logger() 23 | 24 | 25 | def _try_import_pyrfc(): 26 | 27 | # pylint: disable=global-statement 28 | global SAPRFC_MODULE 29 | 30 | try: 31 | import pyrfc 32 | except ImportError as ex: 33 | mod_log().info('Failed to import the module pyrfc') 34 | mod_log().debug(str(ex)) 35 | else: 36 | SAPRFC_MODULE = pyrfc 37 | 38 | 39 | def _unimport_pyrfc(): 40 | """For the sake of testing""" 41 | 42 | # pylint: disable=global-statement 43 | global SAPRFC_MODULE 44 | 45 | SAPRFC_MODULE = None 46 | 47 | 48 | def rfc_is_available(): 49 | """Returns true if RFC can be used""" 50 | 51 | if SAPRFC_MODULE is None: 52 | _try_import_pyrfc() 53 | 54 | return SAPRFC_MODULE is not None 55 | 56 | 57 | def _assert_rfc_availability(): 58 | if not rfc_is_available(): 59 | raise sap.errors.SAPCliError( 60 | 'RFC functionality is not available(enabled)') 61 | 62 | 63 | def connect(**kwargs): 64 | """SAP RFC Connection. 65 | """ 66 | 67 | _assert_rfc_availability() 68 | 69 | mod_log().info('Connecting via SAP rfc with params %s', kwargs) 70 | # pylint: disable=protected-access 71 | try: 72 | return SAPRFC_MODULE.Connection(**kwargs) 73 | except SAPRFC_MODULE._exception.LogonError as exc: 74 | raise RFCLoginError(kwargs['ashost'], kwargs['user'], exc) from exc 75 | except SAPRFC_MODULE._exception.CommunicationError as exc: 76 | raise RFCCommunicationError(kwargs['ashost'], kwargs['user'], exc) from exc 77 | 78 | 79 | def try_pyrfc_exception_type(): 80 | """SAP RFC Exception type. 81 | """ 82 | 83 | _assert_rfc_availability() 84 | 85 | # pylint: disable=protected-access 86 | return SAPRFC_MODULE._exception.RFCLibError 87 | -------------------------------------------------------------------------------- /sap/rfc/errors.py: -------------------------------------------------------------------------------- 1 | """Wrappers for RFC module errors""" 2 | from sap.errors import SAPCliError 3 | 4 | 5 | class RFCConnectionError(SAPCliError): 6 | """Wrapper for RFC connection error""" 7 | 8 | def __init__(self, host, user, message): 9 | self.message = f'[HOST:"{host}", USER:"{user}"] Error: {message}' 10 | 11 | def __str__(self): 12 | return f'RFC connection error: {self.message}' 13 | 14 | 15 | class RFCLoginError(RFCConnectionError): 16 | """Wrapper for RFC Login error""" 17 | 18 | def __init__(self, host, user, exception): 19 | super().__init__(host, user, exception.message) 20 | 21 | 22 | class RFCCommunicationError(RFCConnectionError): 23 | """Wrapper for RFC Communication error""" 24 | 25 | def __init__(self, host, user, exception): 26 | msg = exception.message.split('\n') 27 | msg = next(filter(lambda line: line.startswith('ERROR'), msg)) 28 | msg = ' '.join(msg.split()[1:]) 29 | super().__init__(host, user, msg) 30 | -------------------------------------------------------------------------------- /sapcli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This script is just a wrapper that configures PYTHONPATH 3 | # and runs the python script bin/sapcli 4 | # Its only purpose is to simplify testing of the tool when checked out from a 5 | # git repository. 6 | 7 | import os 8 | import sys 9 | 10 | import importlib.util 11 | from importlib.machinery import SourceFileLoader 12 | 13 | base_dir = os.path.dirname(os.path.abspath(os.path.realpath(__file__))) 14 | sys.path.append(base_dir) 15 | 16 | sapcli_bin = os.path.join(base_dir, 'bin', 'sapcli') 17 | loader = SourceFileLoader(fullname='sapcli', path=sapcli_bin) 18 | spec = importlib.util.spec_from_file_location('sapcli', sapcli_bin, loader=loader) 19 | sapcli = importlib.util.module_from_spec(spec) 20 | spec.loader.exec_module(sapcli) 21 | sys.modules['sapcli'] = sapcli 22 | 23 | sys.exit(sapcli.main(sys.argv)) 24 | -------------------------------------------------------------------------------- /sapcli.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set BASE_DIR=%~p0% 3 | set PYFILE=%BASE_DIR%\bin\sapcli 4 | if "%PYTHONPATH%"=="" ( 5 | set PYTHONPATH=%BASE_DIR% 6 | ) else ( 7 | if "x!PYTHONPATH:%BASE_DIR%=!"=="x%PYTHONPATH%" ( 8 | set PYTHONPATH=%PYTHONPATH%;%BASE_DIR% 9 | ) 10 | ) 11 | echo %PYTHONPATH% 12 | "py" -3 "%PYFILE%" %* -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import setuptools 3 | 4 | HERE = os.path.abspath(os.path.dirname(__file__)) 5 | def _read(name): 6 | with open(os.path.join(HERE, name), 'r', encoding='utf-8') as f: 7 | return f.read() 8 | 9 | setuptools.setup( 10 | name="sapcli", 11 | version="1.0.0", 12 | author="Jakub Filak", 13 | description="Command line interface to SAP products", 14 | long_description=_read("README.md"), 15 | long_description_content_type="text/markdown", 16 | packages=setuptools.find_packages(exclude=("test")), 17 | classifiers=[ 18 | "Programming Language :: Python :: 3", 19 | "Development Status :: 6 - Mature", 20 | "Environment :: Console", 21 | "Intended Audience :: Developers", 22 | "License :: OSI Approved :: Apache Software License", 23 | "Operating System :: OS Independent", 24 | "Topic :: Software Development", 25 | "Topic :: System :: Systems Administration", 26 | ], 27 | python_requires=">=3.6", 28 | install_requires=[ 29 | "requests", 30 | "pyodata", 31 | "PyYAML", 32 | ], 33 | test_requires=[ 34 | "codecov", 35 | "flake8", 36 | "pylint", 37 | "pytest>=2.7.0", 38 | "pytest-cov", 39 | ] 40 | ) 41 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfilak/sapcli/229d58a3efeffec7b3b4afaf4fb069f8d83b9c5a/test/__init__.py -------------------------------------------------------------------------------- /test/system/config.sh: -------------------------------------------------------------------------------- 1 | export SAP_USER=DEVELOPER 2 | export SAP_PASSWORD=Down1oad 3 | export SAP_ASHOST=localhost 4 | export SAP_CLIENT=001 5 | export SAP_PORT=8000 6 | export SAP_SSL=false 7 | -------------------------------------------------------------------------------- /test/system/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function phaseStart 4 | { 5 | echo "Phase:: $@" 6 | } 7 | 8 | function phaseEnd 9 | { 10 | echo "Phase:: End" 11 | } 12 | 13 | function setupStepStart 14 | { 15 | echo "Test:: $@" 16 | } 17 | 18 | function setupStepEnd 19 | { 20 | echo "Test:: End" 21 | } 22 | 23 | function testStart 24 | { 25 | echo "Test:: $@" 26 | } 27 | 28 | function testEnd 29 | { 30 | echo "Test:: End" 31 | } 32 | 33 | source ./config.sh 34 | 35 | phaseStart "Setup" 36 | for setup_step in $( ls -A1 setup/ ); do 37 | setupStepStart ${setup_step} 38 | source setup/${setup_step} 39 | setupStepEnd 40 | done 41 | phaseEnd 42 | 43 | phaseStart "Testing" 44 | for test_case in $( ls -A1 test_cases/ ); do 45 | testStart ${test_case} 46 | source test_cases/${test_case} 47 | testEnd 48 | done 49 | phaseEnd 50 | -------------------------------------------------------------------------------- /test/system/setup/00-package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | export SAPCLI_TEST_PACKAGE_ROOT='$sapcli_test_root' 4 | export SAPCLI_TEST_PACKAGE_CHILD='$sapcli_test_child' 5 | 6 | sapcli package create --no-error-existing "${SAPCLI_TEST_PACKAGE_ROOT}" "sapcli test's root package" 7 | sapcli package create --no-error-existing "${SAPCLI_TEST_PACKAGE_CHILD}" "sapcli test's root child" 8 | -------------------------------------------------------------------------------- /test/system/test_cases/50-abap_include.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | set -o nounset 4 | set -o errexit 5 | set -o pipefail 6 | 7 | ABAP_NAME='ztest_inc' 8 | 9 | SAPCLI_OBJECT='include' 10 | 11 | 12 | echo "+++ create +++" 13 | sapcli ${SAPCLI_OBJECT} create ${ABAP_NAME} 'sapcli test' ${SAPCLI_TEST_PACKAGE_ROOT} 14 | 15 | echo "+++ write +++" 16 | echo "* empty source by sapcli" | sapcli ${SAPCLI_OBJECT} write ${ABAP_NAME} - 17 | 18 | echo "+++ activate +++" 19 | sapcli ${SAPCLI_OBJECT} activate ${ABAP_NAME} 20 | 21 | echo "+++ read +++" 22 | sapcli ${SAPCLI_OBJECT} read ${ABAP_NAME} 23 | -------------------------------------------------------------------------------- /test/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfilak/sapcli/229d58a3efeffec7b3b4afaf4fb069f8d83b9c5a/test/unit/__init__.py -------------------------------------------------------------------------------- /test/unit/fixtures_abap.py: -------------------------------------------------------------------------------- 1 | ABAP_GIT_DEFAULT_XML=''' 2 | 3 | 4 | 5 | E 6 | /src/ 7 | PREFIX 8 | 9 | /.travis.yml 10 | /CONTRIBUTING.md 11 | /LICENSE 12 | /README.md 13 | 14 | 15 | 16 | 17 | ''' 18 | -------------------------------------------------------------------------------- /test/unit/fixtures_adt_businessservice.py: -------------------------------------------------------------------------------- 1 | SERVICE_DEFINITION_ADT_XML = ''' 2 | 23 | 29 | 36 | 41 | 46 | ''' 47 | -------------------------------------------------------------------------------- /test/unit/fixtures_adt_checks.py: -------------------------------------------------------------------------------- 1 | ADT_XML_CHECK_REPORTERS = ''' 2 | 3 | 4 | WDYN* 5 | CLAS* 6 | WGRP 7 | 8 | 9 | PROG* 10 | INTF* 11 | HTTP 12 | 13 | 14 | TABL/DT 15 | 16 | ''' 17 | 18 | ADT_XML_RUN_CHECK_2_REPORTERS = ''' 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ''' 49 | -------------------------------------------------------------------------------- /test/unit/fixtures_adt_interface.py: -------------------------------------------------------------------------------- 1 | GET_INTERFACE_ADT_XML=''' 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | X 13 | Standard ABAP (Unicode) 14 | 15 | 16 | 17 | 18 | ''' 19 | -------------------------------------------------------------------------------- /test/unit/fixtures_adt_package.py: -------------------------------------------------------------------------------- 1 | GET_PACKAGE_ADT_XML=''' 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ''' 24 | 25 | GET_PACKAGE_ADT_XML_NOT_FOUND=''' 26 | 27 | 28 | 29 | Error while importing object PKG_NAME from the database. 30 | Error while importing object PKG_NAME from the database. 31 | 32 | 33 | '''.replace('\n', '').replace('\r', '') 34 | -------------------------------------------------------------------------------- /test/unit/fixtures_adt_structure.py: -------------------------------------------------------------------------------- 1 | STRUCTURE_NAME = 'TEST_STRUCTURE' 2 | 3 | STRUCTURE_DEFINITION_ADT_XML = f''' 4 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ''' 31 | 32 | CREATE_STRUCTURE_ADT_XML = f''' 33 | 34 | 35 | ''' 36 | 37 | READ_STRUCTURE_BODY = '''@EndUserText.label : 'Test structure' 38 | @AbapCatalog.enhancement.category : #NOT_EXTENSIBLE 39 | define structure test_structure { 40 | 41 | client : abap.clnt; 42 | foo : abap.clnt; 43 | 44 | }''' 45 | 46 | WRITE_STRUCTURE_BODY = '''@EndUserText.label : 'Test structure' 47 | @AbapCatalog.enhancement.category : #NOT_EXTENSIBLE 48 | define structure test_structure { 49 | 50 | client : abap.clnt; 51 | bar : abap.clnt; 52 | }''' 53 | 54 | FAKE_LOCK_HANDLE = 'lock_handle' 55 | 56 | ACTIVATE_STRUCTURE_BODY = f''' 57 | 58 | 59 | ''' 60 | -------------------------------------------------------------------------------- /test/unit/fixtures_cli_aunit.py: -------------------------------------------------------------------------------- 1 | """ABAP Unit Test Framework ADT fixtures for Command Line Interface""" 2 | 3 | # https://github.com/windyroad/JUnit-Schema/blob/master/JUnit.xsd 4 | # https://github.com/junit-team/junit5/blob/master/platform-tests/src/test/resources/jenkins-junit.xsd 5 | # http://svn.apache.org/repos/asf/ant/core/trunk/src/main/org/apache/tools/ant/taskdefs/optional/junit/ 6 | 7 | AUNIT_NO_TEST_RESULTS_JUNIT = ''' 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | alert 17 | 18 | 19 | stack 20 | 21 | 22 | 23 | 24 | 25 | ''' 26 | -------------------------------------------------------------------------------- /test/unit/fixtures_flp_builder.py: -------------------------------------------------------------------------------- 1 | # Standard configuration 2 | FLP_BUILDER_CONFIG = ''' 3 | catalogs: 4 | - title: Custom Catalog 5 | id: ZCUSTOM_CATALOG 6 | target_mappings: 7 | - title: My Reporting App 8 | semantic_object: MyReporting 9 | semantic_action: display 10 | url: /sap/bc/ui5_ui5/sap/ZMY_REPORT # path to the BSP app on the SAP system 11 | ui5_component: zmy.app.reporting # UI5 app id defined in manifest.json 12 | tiles: 13 | - title: My Reporting App 14 | id: ZMY_REPORTING 15 | icon: sap-icon://settings 16 | semantic_object: MyReporting 17 | semantic_action: display 18 | groups: 19 | - title: Custom Group 20 | id: ZCUSTOM_GROUP 21 | tiles: 22 | - title: My Reporting App 23 | catalog_id: ZCUSTOM_CATALOG 24 | catalog_tile_id: ZMY_REPORTING # this has to match one of the catalogs->tiles->id property 25 | ''' 26 | -------------------------------------------------------------------------------- /test/unit/fixtures_rfc.py: -------------------------------------------------------------------------------- 1 | BAPIRET2_INFO = { 2 | 'FIELD': '', 3 | 'ID': 'CICD_GCTS_TR', 4 | 'LOG_MSG_NO': '000000', 5 | 'LOG_NO': '', 6 | 'MESSAGE': 'Everything is OK', 7 | 'MESSAGE_V1': '', 8 | 'MESSAGE_V2': '', 9 | 'MESSAGE_V3': '', 10 | 'MESSAGE_V4': '', 11 | 'NUMBER': '007', 12 | 'PARAMETER': '', 13 | 'ROW': 0, 14 | 'SYSTEM': '', 15 | 'TYPE': 'I' 16 | } 17 | 18 | BAPIRET2_WARNING = { 19 | 'FIELD': '', 20 | 'ID': 'CICD_GCTS_TR', 21 | 'LOG_MSG_NO': '000000', 22 | 'LOG_NO': '', 23 | 'MESSAGE': 'List of ABAP repository objects (piece list) is empty', 24 | 'MESSAGE_V1': '', 25 | 'MESSAGE_V2': '', 26 | 'MESSAGE_V3': '', 27 | 'MESSAGE_V4': '', 28 | 'NUMBER': '045', 29 | 'PARAMETER': '', 30 | 'ROW': 0, 31 | 'SYSTEM': '', 32 | 'TYPE': 'W' 33 | } 34 | 35 | BAPIRET2_ERROR = { 36 | 'FIELD': '', 37 | 'ID': 'CICD_GCTS_TR', 38 | 'LOG_MSG_NO': '000000', 39 | 'LOG_NO': '', 40 | 'MESSAGE': 'List of ABAP repository objects (piece list) is empty', 41 | 'MESSAGE_V1': '', 42 | 'MESSAGE_V2': '', 43 | 'MESSAGE_V3': '', 44 | 'MESSAGE_V4': '', 45 | 'NUMBER': '045', 46 | 'PARAMETER': '', 47 | 'ROW': 0, 48 | 'SYSTEM': '', 49 | 'TYPE': 'E' 50 | } 51 | -------------------------------------------------------------------------------- /test/unit/fixtures_sap_rest_gcts.py: -------------------------------------------------------------------------------- 1 | from mock import Response 2 | 3 | GCTS_RESPONSE_REPO_NOT_EXISTS = Response.with_json( 4 | status_code=500, 5 | json={ 6 | 'exception': 'No relation between system and repository' 7 | } 8 | ) 9 | 10 | GCTS_RESPONSE_REPO_PULL_OK = Response.with_json( 11 | status_code=200, 12 | json={ 13 | 'fromCommit': '123', 14 | 'toCommit': '456', 15 | 'history': { 16 | 'fromCommit': '123', 17 | 'toCommit': '456', 18 | 'type': 'PULL' 19 | } 20 | } 21 | ) 22 | -------------------------------------------------------------------------------- /test/unit/infra.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | 3 | def generate_parse_args(command_group): 4 | 5 | def parse_args_impl(*argv): 6 | parser = ArgumentParser() 7 | command_group.install_parser(parser) 8 | return parser.parse_args(argv) 9 | 10 | return parse_args_impl 11 | -------------------------------------------------------------------------------- /test/unit/runtest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PARAMS=$@ 4 | if [ $# -eq 1 ]; then 5 | if [ -e $1 ]; then 6 | PARAMS=${1%.py} 7 | fi 8 | fi 9 | 10 | if [ ! -e README.md ]; then 11 | cd ../../ 12 | fi 13 | 14 | PYTHONPATH=./:test/unit ${PYTHON_BIN:-python3} -m unittest ${PARAMS} 15 | -------------------------------------------------------------------------------- /test/unit/test_sap_adt_coverage.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | 3 | import unittest 4 | 5 | import sap 6 | import sap.adt 7 | from fixtures_adt_coverage import ACOVERAGE_RESULTS_XML 8 | from sap.adt.aunit import Alert, AlertSeverity 9 | from sap.adt.objects import ADTObjectSets 10 | 11 | from fixtures_adt import DummyADTObject 12 | from fixtures_adt_aunit import AUNIT_RESULTS_XML, AUNIT_NO_TEST_RESULTS_XML 13 | 14 | from mock import Connection 15 | 16 | 17 | class TestACoverage(unittest.TestCase): 18 | 19 | def test_query_default(self): 20 | connection = Connection() 21 | 22 | victory = DummyADTObject(connection=connection) 23 | 24 | tested_objects = ADTObjectSets() 25 | tested_objects.include_object(victory) 26 | 27 | coverage_identifier = 'FOOBAR' 28 | runner = sap.adt.acoverage.ACoverage(connection) 29 | response = runner.execute(coverage_identifier, tested_objects) 30 | 31 | self.assertEqual(connection.execs[0].body, 32 | ''' 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ''') 42 | 43 | self.assertEqual(connection.execs[0].adt_uri, '/sap/bc/adt/runtime/traces/coverage/measurements/FOOBAR') 44 | 45 | 46 | class TestACoverageParseResults(unittest.TestCase): 47 | 48 | def test_parse_full(self): 49 | root_node = sap.adt.acoverage.parse_acoverage_response(ACOVERAGE_RESULTS_XML).root_node 50 | 51 | self.assertEqual(root_node.name, 'ADT_ROOT_NODE') 52 | 53 | self.assertEqual([node.name for node in root_node.nodes], ['TEST_CHECK_LIST']) 54 | 55 | self.assertEqual([coverage.type for coverage in root_node.nodes[0].coverages], ['branch', 'procedure', 'statement']) 56 | self.assertEqual([coverage.total for coverage in root_node.nodes[0].coverages], [134, 52, 331]) 57 | 58 | self.assertEqual(root_node.nodes[0].nodes[0].nodes[0].nodes[0].name, 'METHOD_A') 59 | 60 | self.assertEqual(len(root_node.nodes[0].nodes), 2) 61 | self.assertEqual(len(root_node.nodes[0].nodes[1].nodes), 0) 62 | 63 | 64 | def test_parse_no_coverage(self): 65 | pass 66 | # TODO implement 67 | 68 | 69 | if __name__ == '__main__': 70 | unittest.main() 71 | -------------------------------------------------------------------------------- /test/unit/test_sap_adt_coverage_statements.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | 3 | import unittest 4 | 5 | from fixtures_adt_coverage import ACOVERAGE_STATEMENTS_RESULTS_XML 6 | from mock import Connection 7 | from sap.adt.acoverage_statements import parse_statements_response, ACoverageStatements, StatementRequest, StatementsBulkRequest 8 | 9 | 10 | class TestACoverageStatements(unittest.TestCase): 11 | 12 | def test_query_default(self): 13 | connection = Connection() 14 | 15 | acoverage_statements = ACoverageStatements(connection) 16 | statement_requests = [StatementRequest(uri) for uri in ['foo', 'bar']] 17 | bulk_statements = StatementsBulkRequest('FOOBAR', statement_requests) 18 | acoverage_statements = ACoverageStatements(connection) 19 | acoverage_statements_response = acoverage_statements.execute(bulk_statements) 20 | 21 | self.assertEqual(connection.execs[0].body, 22 | ''' 23 | 24 | 25 | 26 | ''') 27 | 28 | self.assertEqual(connection.execs[0].adt_uri, '/sap/bc/adt/runtime/traces/coverage/results/FOOBAR/statements') 29 | 30 | 31 | class TestStatementsBulkRequest(unittest.TestCase): 32 | 33 | def test_add_statement_request(self): 34 | bulk_statements = StatementsBulkRequest('FOOBAR') 35 | 36 | statement_requests = [StatementRequest(uri) for uri in ['foo', 'bar']] 37 | 38 | bulk_statements.add_statement_request(statement_requests[0]) 39 | bulk_statements.add_statement_request(statement_requests[1]) 40 | 41 | self.assertEqual(bulk_statements._statement_requests, statement_requests) 42 | 43 | class TestStatementRequest(unittest.TestCase): 44 | 45 | def test_setter(self): 46 | request = StatementRequest('foo') 47 | self.assertEqual(request._get, 'foo') 48 | 49 | request.get = 'bar' 50 | self.assertEqual(request._get, 'bar') 51 | 52 | 53 | class TestACoverageStatementsParseResults(unittest.TestCase): 54 | 55 | def test_parse_full(self): 56 | statement_responses = parse_statements_response(ACOVERAGE_STATEMENTS_RESULTS_XML).statement_responses 57 | 58 | self.assertEqual(len(statement_responses), 2) 59 | self.assertEqual(len(statement_responses[0].statements), 5) 60 | self.assertEqual(len(statement_responses[1].statements), 8) 61 | 62 | self.assertEqual(statement_responses[0].name, "FOO===========================CP.FOO.METHOD_A") 63 | self.assertEqual(statement_responses[0].statements[0].uri, "/sap/bc/adt/oo/classes/foo/source/main#start=53,1;end=53,38") 64 | self.assertEqual(statement_responses[0].statements[0].executed, "4") 65 | 66 | if __name__ == '__main__': 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /test/unit/test_sap_adt_datadefinition.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | import sap.adt 6 | import sap.adt.wb 7 | 8 | from mock import Connection, Response 9 | 10 | from fixtures_adt import GET_DDL_ADT_XML, LOCK_RESPONSE_OK, EMPTY_RESPONSE_OK 11 | 12 | 13 | FIXTURE_ACTIVATION_REQUEST_XML=''' 14 | 15 | 16 | ''' 17 | 18 | FIXTURE_CDS_CODE='''define view MyUsers 19 | as select from usr02 20 | { 21 | } 22 | ''' 23 | 24 | 25 | class TestADTDataDefinition(unittest.TestCase): 26 | 27 | def test_adt_ddl_init(self): 28 | metadata = sap.adt.ADTCoreData() 29 | 30 | ddl = sap.adt.DataDefinition('CONNECTION', name='MyUsers', package='PACKAGE', metadata=metadata) 31 | self.assertEqual(ddl.name, 'MyUsers') 32 | self.assertEqual(ddl.reference.name, 'PACKAGE') 33 | self.assertEqual(ddl.coredata, metadata) 34 | 35 | def test_adt_ddl_read(self): 36 | conn = Connection([Response(text=FIXTURE_CDS_CODE, 37 | status_code=200, 38 | headers={'Content-Type': 'text/plain; charset=utf-8'})]) 39 | 40 | ddl = sap.adt.DataDefinition(conn, name='MyUsers') 41 | code = ddl.text 42 | 43 | self.assertEqual(conn.mock_methods(), [('GET', '/sap/bc/adt/ddic/ddl/sources/myusers/source/main')]) 44 | 45 | get_request = conn.execs[0] 46 | self.assertEqual(sorted(get_request.headers), ['Accept']) 47 | self.assertEqual(get_request.headers['Accept'], 'text/plain') 48 | 49 | self.assertIsNone(get_request.params) 50 | self.assertIsNone(get_request.body) 51 | 52 | self.maxDiff = None 53 | self.assertEqual(code, FIXTURE_CDS_CODE) 54 | 55 | def test_adt_ddl_activate(self): 56 | conn = Connection([ 57 | EMPTY_RESPONSE_OK, 58 | Response(text=GET_DDL_ADT_XML, 59 | status_code=200, 60 | headers={'Content-Type': 'application/xml; charset=utf-8'}) 61 | ]) 62 | 63 | ddl = sap.adt.DataDefinition(conn, name='MyUsers') 64 | sap.adt.wb.activate(ddl) 65 | 66 | self.assertEqual(conn.mock_methods(), [ 67 | ('POST', '/sap/bc/adt/activation'), 68 | ('GET', '/sap/bc/adt/ddic/ddl/sources/myusers') 69 | ]) 70 | 71 | # two requests - activation + fetch 72 | self.assertEqual(len(conn.execs), 2) 73 | 74 | get_request = conn.execs[0] 75 | self.assertEqual(sorted(get_request.headers), ['Accept', 'Content-Type']) 76 | self.assertEqual(get_request.headers['Accept'], 'application/xml') 77 | self.assertEqual(get_request.headers['Content-Type'], 'application/xml') 78 | 79 | self.assertEqual(sorted(get_request.params), ['method', 'preauditRequested']) 80 | self.assertEqual(get_request.params['method'], 'activate') 81 | self.assertEqual(get_request.params['preauditRequested'], 'true') 82 | 83 | self.assertEqual(get_request.body, FIXTURE_ACTIVATION_REQUEST_XML) 84 | 85 | if __name__ == '__main__': 86 | unittest.main() 87 | -------------------------------------------------------------------------------- /test/unit/test_sap_adt_dataelement.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | import sap.adt 5 | import sap.adt.dataelement 6 | 7 | from mock import Connection, Response, Request 8 | from fixtures_adt_dataelement import DATA_ELEMENT_DEFINITION_ADT_XML, DATA_ELEMENT_NAME, CREATE_DATA_ELEMENT_ADT_XML 9 | 10 | 11 | class TestADTDataElement(unittest.TestCase): 12 | 13 | def test_data_element_fetch(self): 14 | connection = Connection([Response(text=DATA_ELEMENT_DEFINITION_ADT_XML, status_code=200, headers={})]) 15 | 16 | data_element = sap.adt.DataElement(connection, DATA_ELEMENT_NAME) 17 | data_element.fetch() 18 | 19 | self.assertEqual(data_element.name, DATA_ELEMENT_NAME) 20 | self.assertEqual(data_element.active, 'active') 21 | self.assertEqual(data_element.master_language, 'EN') 22 | self.assertEqual(data_element.description, 'Test data element') 23 | 24 | def test_data_element_serialize(self): 25 | connection = Connection() 26 | 27 | metadata = sap.adt.ADTCoreData(description='Test data element', language='EN', master_language='EN', 28 | responsible='ANZEIGER') 29 | data_element = sap.adt.DataElement(connection, DATA_ELEMENT_NAME, package='PACKAGE', metadata=metadata) 30 | data_element.create() 31 | 32 | expected_request = Request( 33 | adt_uri='/sap/bc/adt/ddic/dataelements', 34 | method='POST', 35 | headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, 36 | body=bytes(CREATE_DATA_ELEMENT_ADT_XML, 'utf-8'), 37 | params=None 38 | ) 39 | 40 | self.maxDiff = None 41 | expected_request.assertEqual(connection.execs[0], self) 42 | -------------------------------------------------------------------------------- /test/unit/test_sap_adt_datapreview.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | 3 | import unittest 4 | 5 | import sap.adt 6 | import sap 7 | 8 | from fixtures_adt_datapreview import ( 9 | ADT_XML_FREESTYLE_TABLE_T000, 10 | ADT_XML_FREESTYLE_TABLE_T000_ONE_ROW, 11 | ADT_XML_FREESTYLE_TABLE_T000_4_ROWS_NO_TOTAL, 12 | ADT_XML_FREESTYLE_COUNT_ZERO 13 | ) 14 | 15 | 16 | class TestFreeStyleTableParseResults(unittest.TestCase): 17 | 18 | def test_parse_full(self): 19 | clients = sap.adt.datapreview.parse_freestyle_table(ADT_XML_FREESTYLE_TABLE_T000, rows=100) 20 | 21 | self.maxDiff = None 22 | 23 | self.assertEqual(clients, [{'ADRNR': '', 24 | 'CCCATEGORY': 'S', 25 | 'CCCOPYLOCK': '', 26 | 'CCCORACTIV': '1', 27 | 'CCIMAILDIS': '', 28 | 'CCNOCASCAD': '', 29 | 'CCNOCLIIND': '', 30 | 'CCORIGCONT': '', 31 | 'CCSOFTLOCK': '', 32 | 'CCTEMPLOCK': '', 33 | 'CHANGEDATE': '20171218', 34 | 'CHANGEUSER': 'DDIC', 35 | 'LOGSYS': '', 36 | 'MANDT': '000', 37 | 'MTEXT': 'SAP SE', 38 | 'MWAER': 'EUR', 39 | 'ORT01': 'Walldorf & Brno < London'}, 40 | {'ADRNR': '', 41 | 'CCCATEGORY': 'C', 42 | 'CCCOPYLOCK': '', 43 | 'CCCORACTIV': '1', 44 | 'CCIMAILDIS': '', 45 | 'CCNOCASCAD': '', 46 | 'CCNOCLIIND': '', 47 | 'CCORIGCONT': '', 48 | 'CCSOFTLOCK': '', 49 | 'CCTEMPLOCK': '', 50 | 'CHANGEDATE': '00000000', 51 | 'CHANGEUSER': '', 52 | 'LOGSYS': 'NPLCLNT001', 53 | 'MANDT': '001', 54 | 'MTEXT': 'SAP SE', 55 | 'MWAER': 'EUR', 56 | 'ORT01': 'Walldorf'}]) 57 | 58 | def test_parse_not_all_rows(self): 59 | clients = sap.adt.datapreview.parse_freestyle_table(ADT_XML_FREESTYLE_TABLE_T000_ONE_ROW, rows=1) 60 | self.assertEqual(clients, [{'MANDT': '000'}]) 61 | 62 | def test_parse_740(self): 63 | clients = sap.adt.datapreview.parse_freestyle_table(ADT_XML_FREESTYLE_TABLE_T000_4_ROWS_NO_TOTAL, rows=100) 64 | 65 | self.assertEqual(clients, [{'MANDT': '000'}, {'MANDT': '001'}, {'MANDT': '002'}, {'MANDT': '003'}]) 66 | 67 | def test_parse_count_zero(self): 68 | clients = sap.adt.datapreview.parse_freestyle_table(ADT_XML_FREESTYLE_COUNT_ZERO, rows=1) 69 | 70 | # Yes! ADT returns the trailing ' ' and I don't think it's OK to trim all values automatically. 71 | self.assertEqual(clients, [{'COUNT': '0 '}]) 72 | 73 | 74 | if __name__ == '__main__': 75 | unittest.main() 76 | -------------------------------------------------------------------------------- /test/unit/test_sap_adt_errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | import sap.adt.errors 6 | 7 | from fixtures_adt import ( 8 | ERROR_XML_PACKAGE_ALREADY_EXISTS, 9 | ERROR_XML_PROGRAM_ALREADY_EXISTS, 10 | ERROR_XML_MADEUP_PROBLEM 11 | ) 12 | 13 | from fixtures_adt_package import GET_PACKAGE_ADT_XML_NOT_FOUND 14 | 15 | 16 | class TestADTError(unittest.TestCase): 17 | 18 | def test_returns_none_for_invalid_data(self): 19 | error = sap.adt.errors.new_adt_error_from_xml('whatever') 20 | self.assertIsNone(error) 21 | 22 | def test_parse_existing_package(self): 23 | error = sap.adt.errors.new_adt_error_from_xml(ERROR_XML_PACKAGE_ALREADY_EXISTS) 24 | 25 | self.assertEqual(error.namespace, 'com.sap.adt') 26 | self.assertEqual(error.type, 'ExceptionResourceAlreadyExists') 27 | self.assertEqual(error.message, 'Resource Package $SAPCLI_TEST_ROOT does already exist.') 28 | 29 | self.assertEqual(str(error), error.message) 30 | self.assertEqual(repr(error), 'com.sap.adt.ExceptionResourceAlreadyExists') 31 | self.assertIsInstance(error, sap.adt.errors.ExceptionResourceAlreadyExists) 32 | 33 | def test_parse_existing_program(self): 34 | error = sap.adt.errors.new_adt_error_from_xml(ERROR_XML_PROGRAM_ALREADY_EXISTS) 35 | 36 | self.assertEqual(error.namespace, 'com.sap.adt') 37 | self.assertEqual(error.type, 'ExceptionResourceCreationFailure') 38 | self.assertEqual(error.message, 'A program or include already exists with the name SAPCLI_TEST_REPORT.') 39 | 40 | self.assertEqual(str(error), error.message) 41 | self.assertEqual(repr(error), 'com.sap.adt.ExceptionResourceCreationFailure') 42 | self.assertIsInstance(error, sap.adt.errors.ExceptionResourceCreationFailure) 43 | 44 | 45 | def test_parse_not_found_package(self): 46 | error = sap.adt.errors.new_adt_error_from_xml(GET_PACKAGE_ADT_XML_NOT_FOUND) 47 | 48 | self.assertEqual(error.namespace, 'com.sap.adt') 49 | self.assertEqual(error.type, 'ExceptionResourceNotFound') 50 | self.assertEqual(error.message, 'Error while importing object PKG_NAME from the database.') 51 | 52 | self.assertEqual(str(error), error.message) 53 | self.assertEqual(repr(error), 'com.sap.adt.ExceptionResourceNotFound') 54 | self.assertIsInstance(error, sap.adt.errors.ExceptionResourceNotFound) 55 | 56 | def test_parse_arbitrary_error(self): 57 | error = sap.adt.errors.new_adt_error_from_xml(ERROR_XML_MADEUP_PROBLEM) 58 | 59 | self.assertEqual(error.namespace, 'org.example.whatever') 60 | self.assertEqual(error.type, 'UnitTestSAPCLI') 61 | self.assertEqual(error.message, 'Made up problem.') 62 | 63 | self.assertEqual(str(error), 'UnitTestSAPCLI: Made up problem.') 64 | self.assertEqual(repr(error), 'org.example.whatever.UnitTestSAPCLI') 65 | self.assertIsInstance(error, sap.adt.errors.ADTError) 66 | 67 | 68 | if __name__ == '__main__': 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /test/unit/test_sap_adt_interface.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | import sap.adt 6 | 7 | from mock import Connection, Response 8 | 9 | from fixtures_adt import LOCK_RESPONSE_OK, EMPTY_RESPONSE_OK 10 | from fixtures_adt_interface import GET_INTERFACE_ADT_XML 11 | 12 | 13 | FIXTURE_ELEMENTARY_IFACE_XML=''' 14 | 15 | 16 | ''' 17 | 18 | FIXTURE_IFACE_MAIN_CODE='''interface zif_hello_world public . 19 | methods greet. 20 | endinterface. 21 | ''' 22 | 23 | 24 | class TestADTIFace(unittest.TestCase): 25 | 26 | def test_adt_iface_serialize(self): 27 | conn = Connection() 28 | 29 | metadata = sap.adt.ADTCoreData(language='EN', master_language='EN', master_system='NPL', responsible='FILAK') 30 | iface = sap.adt.Interface(conn, 'ZIF_HELLO_WORLD', package='$TEST', metadata=metadata) 31 | iface.description = 'Say hello!' 32 | iface.create() 33 | 34 | self.assertEqual(len(conn.execs), 1) 35 | 36 | self.assertEqual(conn.execs[0][0], 'POST') 37 | self.assertEqual(conn.execs[0][1], '/sap/bc/adt/oo/interfaces') 38 | self.assertEqual(conn.execs[0][2], {'Content-Type': 'application/vnd.sap.adt.oo.interfaces.v5+xml; charset=utf-8'}) 39 | self.maxDiff = None 40 | self.assertEqual(conn.execs[0][3].decode('utf-8'), FIXTURE_ELEMENTARY_IFACE_XML) 41 | 42 | def test_adt_iface_write(self): 43 | conn = Connection([LOCK_RESPONSE_OK, EMPTY_RESPONSE_OK, None]) 44 | 45 | iface = sap.adt.Interface(conn, 'ZIF_HELLO_WORLD') 46 | 47 | with iface.open_editor() as editor: 48 | editor.write(FIXTURE_IFACE_MAIN_CODE) 49 | 50 | put_request = conn.execs[1] 51 | 52 | self.assertEqual(put_request.method, 'PUT') 53 | self.assertEqual(put_request.adt_uri, '/sap/bc/adt/oo/interfaces/zif_hello_world/source/main') 54 | 55 | self.assertEqual(sorted(put_request.headers), ['Content-Type']) 56 | self.assertEqual(put_request.headers['Content-Type'], 'text/plain; charset=utf-8') 57 | 58 | self.assertEqual(sorted(put_request.params), ['lockHandle']) 59 | self.assertEqual(put_request.params['lockHandle'], 'win') 60 | 61 | self.maxDiff = None 62 | self.assertEqual(put_request.body, bytes(FIXTURE_IFACE_MAIN_CODE[:-1], 'utf-8')) 63 | 64 | def test_adt_class_fetch(self): 65 | conn = Connection([Response(text=GET_INTERFACE_ADT_XML, 66 | status_code=200, 67 | headers={'Content-Type': 'application/vnd.sap.adt.oo.interfaces.v2+xml; charset=utf-8'})]) 68 | 69 | intf = sap.adt.Interface(conn, 'ZIF_HELLO_WORLD') 70 | intf.fetch() 71 | 72 | self.assertEqual(intf.name, 'ZIF_HELLO_WORLD') 73 | self.assertEqual(intf.active, 'active') 74 | self.assertEqual(intf.master_language, 'EN') 75 | self.assertEqual(intf.description, 'You cannot stop me!') 76 | self.assertEqual(intf.modeled, False) 77 | 78 | 79 | if __name__ == '__main__': 80 | unittest.main() 81 | -------------------------------------------------------------------------------- /test/unit/test_sap_adt_object_factory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | from unittest.mock import Mock 5 | 6 | from sap.errors import SAPCliError 7 | from sap.adt.object_factory import ( 8 | ADTObjectFactory, 9 | human_names_factory 10 | ) 11 | 12 | 13 | 14 | class TestgADTObjectFactory(unittest.TestCase): 15 | 16 | def setUp(self): 17 | self.mock_conn = Mock() 18 | 19 | def test_factory_init_default(self): 20 | factory = ADTObjectFactory(self.mock_conn) 21 | 22 | self.assertEqual(factory._connection, self.mock_conn) 23 | self.assertEqual(list(factory.get_supported_names()), []) 24 | 25 | def test_factory_init_with_builders(self): 26 | exp_builders = {'fabulous': 'awesome'} 27 | factory = ADTObjectFactory(self.mock_conn, builders=exp_builders) 28 | 29 | self.assertEqual(factory._connection, self.mock_conn) 30 | self.assertEqual(list(factory.get_supported_names()), ['fabulous']) 31 | 32 | def test_factory_register_new(self): 33 | exp_producer = Mock() 34 | 35 | factory = ADTObjectFactory(self.mock_conn) 36 | factory.register('fabulous', exp_producer) 37 | product = factory.make('fabulous', 'awesome') 38 | 39 | self.assertEqual(product, exp_producer.return_value) 40 | exp_producer.assert_called_once_with(self.mock_conn, 'awesome') 41 | 42 | def test_factory_register_existing_raise(self): 43 | exp_producer = Mock() 44 | 45 | factory = ADTObjectFactory(self.mock_conn, builders={'fabulous': exp_producer}) 46 | with self.assertRaises(SAPCliError) as caught: 47 | factory.register('fabulous', exp_producer) 48 | 49 | self.assertEqual(str(caught.exception), 'Object type builder was already registered: fabulous') 50 | 51 | def test_factory_register_existing_overwrote(self): 52 | def_producer = Mock() 53 | def_producer.side_effect = Exception 54 | 55 | exp_another_producer = Mock() 56 | 57 | factory = ADTObjectFactory(self.mock_conn, builders={'fabulous': def_producer}) 58 | factory.register('fabulous', exp_another_producer, overwrite=True) 59 | product = factory.make('fabulous', 'awesome') 60 | 61 | self.assertEqual(product, exp_another_producer.return_value) 62 | exp_another_producer.assert_called_once_with(self.mock_conn, 'awesome') 63 | 64 | def test_factory_make_unknown(self): 65 | exp_builders = {'fabulous': 'awesome'} 66 | factory = ADTObjectFactory(self.mock_conn, builders=exp_builders) 67 | 68 | with self.assertRaises(SAPCliError) as caught: 69 | product = factory.make('gorgeous', 'success') 70 | 71 | self.assertEqual(str(caught.exception), 'Unknown ADT object type: gorgeous') 72 | 73 | def test_factory_make_known(self): 74 | mock_producer = Mock() 75 | exp_builders = {'fabulous': mock_producer} 76 | factory = ADTObjectFactory(self.mock_conn, builders=exp_builders) 77 | 78 | product = factory.make('fabulous', 'success') 79 | 80 | self.assertEqual(product, mock_producer.return_value) 81 | mock_producer.assert_called_once_with(self.mock_conn, 'success') 82 | -------------------------------------------------------------------------------- /test/unit/test_sap_adt_program.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | 3 | import unittest 4 | 5 | import sap.adt 6 | 7 | from mock import Connection, Response 8 | from fixtures_adt import LOCK_RESPONSE_OK, EMPTY_RESPONSE_OK 9 | 10 | from fixtures_adt_program import CREATE_EXECUTABLE_PROGRAM_ADT_XML, GET_EXECUTABLE_PROGRAM_ADT_XML 11 | 12 | FIXTURE_REPORT_CODE='report zhello_world.\n\n write: \'Hello, World!\'.\n' 13 | 14 | 15 | class TestADTProgram(unittest.TestCase): 16 | 17 | def test_program_serialize(self): 18 | conn = Connection() 19 | 20 | metadata = sap.adt.ADTCoreData(language='EN', master_language='EN', master_system='NPL', responsible='FILAK') 21 | program = sap.adt.Program(conn, 'ZHELLO_WORLD', package='$TEST', metadata=metadata) 22 | program.description = 'Say hello!' 23 | program.create() 24 | 25 | self.assertEqual(len(conn.execs), 1) 26 | 27 | self.assertEqual(conn.execs[0][0], 'POST') 28 | self.assertEqual(conn.execs[0][1], '/sap/bc/adt/programs/programs') 29 | self.assertEqual(conn.execs[0][2], {'Content-Type': 'application/vnd.sap.adt.programs.programs.v2+xml; charset=utf-8'}) 30 | self.maxDiff = None 31 | self.assertEqual(conn.execs[0][3].decode('utf-8'), CREATE_EXECUTABLE_PROGRAM_ADT_XML) 32 | 33 | def test_program_write(self): 34 | conn = Connection([LOCK_RESPONSE_OK, EMPTY_RESPONSE_OK, None]) 35 | 36 | program = sap.adt.Program(conn, 'ZHELLO_WORLD') 37 | with program.open_editor() as editor: 38 | editor.write(FIXTURE_REPORT_CODE) 39 | 40 | self.assertEqual(len(conn.execs), 3) 41 | 42 | put_request = conn.execs[1] 43 | 44 | self.assertEqual(put_request.method, 'PUT') 45 | self.assertEqual(put_request.adt_uri, '/sap/bc/adt/programs/programs/zhello_world/source/main') 46 | self.assertEqual(put_request.headers, {'Content-Type': 'text/plain; charset=utf-8'}) 47 | self.assertEqual(put_request.params, {'lockHandle': 'win'}) 48 | 49 | self.maxDiff = None 50 | self.assertEqual(put_request.body, bytes(FIXTURE_REPORT_CODE[:-1], 'utf-8')) 51 | 52 | def test_program_write_with_corrnr(self): 53 | conn = Connection([LOCK_RESPONSE_OK, EMPTY_RESPONSE_OK, None]) 54 | 55 | program = sap.adt.Program(conn, 'ZHELLO_WORLD') 56 | with program.open_editor(corrnr='420') as editor: 57 | editor.write(FIXTURE_REPORT_CODE) 58 | 59 | put_request = conn.execs[1] 60 | self.assertEqual(put_request.params, {'lockHandle': 'win', 'corrNr': '420'}) 61 | 62 | def test_adt_program_fetch(self): 63 | conn = Connection([Response(text=GET_EXECUTABLE_PROGRAM_ADT_XML, status_code=200, headers={})]) 64 | program = sap.adt.Program(conn, 'ZHELLO_WORLD') 65 | program.fetch() 66 | 67 | self.assertEqual(program.name, 'ZHELLO_WORLD') 68 | self.assertEqual(program.active, 'active') 69 | self.assertEqual(program.program_type, '1') 70 | self.assertEqual(program.master_language, 'EN') 71 | self.assertEqual(program.description, 'Say hello!') 72 | self.assertEqual(program.logical_database.reference.name, 'D$S') 73 | self.assertEqual(program.fix_point_arithmetic, True) 74 | self.assertEqual(program.case_sensitive, True) 75 | self.assertEqual(program.application_database, 'S') 76 | 77 | 78 | if __name__ == '__main__': 79 | unittest.main() 80 | -------------------------------------------------------------------------------- /test/unit/test_sap_adt_search.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from sap.adt.search import ADTSearch 6 | 7 | from mock import Connection, Response, Request 8 | 9 | 10 | FIXTURE_ADT_SEARCH_RESPONSE_FOUND=""" 11 | 12 | 13 | 14 | """ 15 | 16 | 17 | class TestADTSearch(unittest.TestCase): 18 | 19 | def setUp(self): 20 | self.mock_conn = Connection() 21 | 22 | def test_init(self): 23 | search = ADTSearch(self.mock_conn) 24 | self.assertEqual(search._connection, self.mock_conn) 25 | 26 | def test_quick_search(self): 27 | exp_search_term = 'SOME?ABAP& OBJEC' 28 | 29 | self.mock_conn.set_responses([ 30 | Response(status_code=200, text=FIXTURE_ADT_SEARCH_RESPONSE_FOUND), 31 | ]) 32 | 33 | search = ADTSearch(self.mock_conn) 34 | result = search.quick_search(exp_search_term) 35 | 36 | self.mock_conn.execs[0].assertEqual(Request.get( 37 | adt_uri='/sap/bc/adt/repository/informationsystem/search', 38 | params={ 39 | 'operation': 'quickSearch', 40 | 'query': exp_search_term, 41 | 'maxResults': 5, 42 | } 43 | ), self) 44 | 45 | self.assertEqual(result.references[0].typ, 'PROG/I') 46 | self.assertEqual(result.references[0].name, 'JAKUB_IS_AWESOME') 47 | -------------------------------------------------------------------------------- /test/unit/test_sap_adt_structure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import logging 4 | import unittest 5 | import sap.adt 6 | 7 | from mock import Connection, Response, Request 8 | from fixtures_adt_structure import STRUCTURE_DEFINITION_ADT_XML, STRUCTURE_NAME, CREATE_STRUCTURE_ADT_XML 9 | 10 | 11 | class TestADTStructure(unittest.TestCase): 12 | 13 | def test_structure_fetch(self): 14 | connection = Connection([Response(text=STRUCTURE_DEFINITION_ADT_XML, status_code=200, headers={})]) 15 | 16 | structure = sap.adt.Structure(connection, STRUCTURE_NAME) 17 | structure.fetch() 18 | 19 | self.assertEqual(structure.name, STRUCTURE_NAME) 20 | self.assertEqual(structure.active, 'active') 21 | self.assertEqual(structure.master_language, 'EN') 22 | self.assertEqual(structure.description, 'Test structure') 23 | 24 | def test_structure_serialize(self): 25 | connection = Connection() 26 | 27 | metadata = sap.adt.ADTCoreData(description='Test structure', language='EN', master_language='EN', 28 | responsible='ANZEIGER') 29 | structure = sap.adt.Structure(connection, STRUCTURE_NAME, package='PACKAGE', metadata=metadata) 30 | structure.create() 31 | 32 | expected_request = Request( 33 | adt_uri='/sap/bc/adt/ddic/structures', 34 | method='POST', 35 | headers={'Content-Type': 'application/vnd.sap.adt.structures.v2+xml; charset=utf-8'}, 36 | body=bytes(CREATE_STRUCTURE_ADT_XML, 'utf-8'), 37 | params=None 38 | ) 39 | 40 | self.assertEqual(connection.execs[0], expected_request) 41 | -------------------------------------------------------------------------------- /test/unit/test_sap_adt_table.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | import sap.adt 5 | 6 | from mock import Connection, Response, Request 7 | from fixtures_adt_table import TABLE_DEFINITION_ADT_XML, TABLE_NAME, CREATE_TABLE_ADT_XML 8 | 9 | 10 | class TestADTTable(unittest.TestCase): 11 | 12 | def test_table_fetch(self): 13 | connection = Connection([Response(text=TABLE_DEFINITION_ADT_XML, status_code=200, headers={})]) 14 | 15 | table = sap.adt.Table(connection, TABLE_NAME) 16 | table.fetch() 17 | 18 | self.assertEqual(table.name, TABLE_NAME) 19 | self.assertEqual(table.active, 'active') 20 | self.assertEqual(table.master_language, 'EN') 21 | self.assertEqual(table.description, 'Test table') 22 | 23 | def test_table_serialize(self): 24 | connection = Connection() 25 | 26 | metadata = sap.adt.ADTCoreData(description='Test table', language='EN', master_language='EN', 27 | responsible='ANZEIGER') 28 | table = sap.adt.Table(connection, TABLE_NAME, package='PACKAGE', metadata=metadata) 29 | table.create() 30 | 31 | expected_request = Request( 32 | adt_uri='/sap/bc/adt/ddic/tables', 33 | method='POST', 34 | headers={'Content-Type': 'application/vnd.sap.adt.tables.v2+xml; charset=utf-8'}, 35 | body=bytes(CREATE_TABLE_ADT_XML, 'utf-8'), 36 | params=None 37 | ) 38 | self.assertEqual(connection.execs[0], expected_request) 39 | -------------------------------------------------------------------------------- /test/unit/test_sap_cli.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | 3 | import sys 4 | 5 | import unittest 6 | from unittest.mock import patch, MagicMock 7 | 8 | import sap.cli 9 | import sap.cli.core 10 | 11 | 12 | class TestModule(unittest.TestCase): 13 | 14 | def test_get_commands_blind(self): 15 | commands = sap.cli.get_commands() 16 | self.assertTrue(commands, msg='Some commands should be registered') 17 | 18 | for idx, cmd in enumerate(commands): 19 | self.assertEqual(len(cmd), 2, 20 | msg='The command should be 2 tuple - Command: ' + str(idx)) 21 | 22 | self.assertTrue(callable(cmd[0]), 23 | msg='The first item should be callable - Command: ' + str(idx)) 24 | 25 | self.assertIsInstance(cmd[1], sap.cli.core.CommandGroup, 26 | msg='The second item should be of a command group - Command: ' + str(idx)) 27 | 28 | 29 | class TestPrinting(unittest.TestCase): 30 | 31 | def test_get_console_returns_global(self): 32 | self.assertEqual(sap.cli.core.get_console(), sap.cli.core._CONSOLE) 33 | self.assertIsNotNone(sap.cli.core.get_console()) 34 | 35 | def test_printout_sanity(self): 36 | console = MagicMock() 37 | 38 | with patch('sap.cli.core.get_console') as fake_get_console: 39 | fake_get_console.return_value = console 40 | 41 | sap.cli.core.printout('a', 'b', sep=':', end='$') 42 | sap.cli.core.printerr('e', 'r', sep='-', end='!') 43 | 44 | self.assertEqual(2, len(fake_get_console.call_args)) 45 | console.printout.assert_called_once_with('a', 'b', sep=':', end='$') 46 | console.printerr.assert_called_once_with('e', 'r', sep='-', end='!') 47 | 48 | def test_printconsole_sanity_printout(self): 49 | console = sap.cli.core.PrintConsole() 50 | 51 | with patch('sap.cli.core.print') as fake_print: 52 | console.printout('a', 'b', sep=':', end='$') 53 | 54 | fake_print.assert_called_once_with('a', 'b', sep=':', end='$', file=sys.stdout) 55 | 56 | def test_printconsole_sanity_printerr(self): 57 | console = sap.cli.core.PrintConsole() 58 | 59 | with patch('sap.cli.core.print') as fake_print: 60 | console.printerr('a', 'b', sep=':', end='$') 61 | 62 | fake_print.assert_called_once_with('a', 'b', sep=':', end='$', file=sys.stderr) 63 | 64 | 65 | class TestConnection(unittest.TestCase): 66 | 67 | def test_empty_instance(self): 68 | args = sap.cli.build_empty_connection_values() 69 | self.assertEqual(args.ashost, None) 70 | self.assertEqual(args.sysnr, None) 71 | self.assertEqual(args.client, None) 72 | self.assertEqual(args.port, None) 73 | self.assertEqual(args.ssl, None) 74 | self.assertEqual(args.verify, None) 75 | self.assertEqual(args.user, None) 76 | self.assertEqual(args.password, None) 77 | self.assertEqual(args.corrnr, None) 78 | 79 | def test_edit_instance(self): 80 | args = sap.cli.build_empty_connection_values() 81 | 82 | args.ashost = 'args.ashost' 83 | self.assertEqual(args.ashost, 'args.ashost') 84 | 85 | args.sysnr = 'args.sysnr' 86 | self.assertEqual(args.sysnr, 'args.sysnr') 87 | 88 | args.client = 'args.client' 89 | self.assertEqual(args.client, 'args.client') 90 | 91 | args.port = 'args.port' 92 | self.assertEqual(args.port, 'args.port') 93 | 94 | args.ssl = 'args.ssl' 95 | self.assertEqual(args.ssl, 'args.ssl') 96 | 97 | args.verify = 'args.verify' 98 | self.assertEqual(args.verify, 'args.verify') 99 | 100 | args.user = 'args.user' 101 | self.assertEqual(args.user, 'args.user') 102 | 103 | args.password = 'args.password' 104 | self.assertEqual(args.password, 'args.password') 105 | 106 | args.corrnr = 'args.corrnr' 107 | self.assertEqual(args.corrnr, 'args.corrnr') 108 | 109 | 110 | if __name__ == '__main__': 111 | unittest.main() 112 | -------------------------------------------------------------------------------- /test/unit/test_sap_cli_activation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | from unittest.mock import call, patch, Mock, PropertyMock, MagicMock, mock_open 5 | from io import StringIO 6 | 7 | import sap.adt.wb 8 | import sap.cli.activation 9 | 10 | from mock import Connection, Response, GroupArgumentParser, patch_get_print_console_with_buffer 11 | 12 | from fixtures_adt_wb import RESPONSE_INACTIVE_OBJECTS_V1 13 | 14 | 15 | INACTIVEOBJECTS_PARSER = GroupArgumentParser(sap.cli.activation.InactiveObjectsGroup) 16 | 17 | 18 | class IOCObjectListBuilder: 19 | 20 | def __init__(self): 21 | self.objects = sap.adt.wb.IOCList() 22 | 23 | def create_basic_object(self): 24 | ioc_object = sap.adt.wb.IOCEntry() 25 | ioc_object.object = sap.adt.wb.IOCEntryData() 26 | ioc_object.object.user = 'FILAK' 27 | ioc_object.object.linked = '' 28 | ioc_object.object.deleted = '' 29 | 30 | return ioc_object 31 | 32 | def populate_class_reference(self, reference, name): 33 | reference.name = f'{name.upper()}' 34 | reference.uri = f'/sap/bc/adt/classes/{name.lower()}' 35 | reference.parent_uri = None 36 | reference.typ = 'CLAS/OC' 37 | reference.description = f'Class {name}' 38 | 39 | def populate_class_method_reference(self, reference, name, ioc_class): 40 | reference.name = f'{ioc_class.object.name}==={name.upper()}' 41 | reference.uri = f'{ioc_class.object.uri};method={name.lower()}' 42 | reference.parent_uri = ioc_class.object.uri 43 | reference.typ = f'{ioc_class.object.typ}/M' 44 | reference.description = f'Method {name} of {ioc_class.object.name}' 45 | 46 | def add_class(self, name): 47 | ioc_class = self.create_basic_object() 48 | self.populate_class_reference(ioc_class.object.reference, name) 49 | 50 | self.objects.entries.append(ioc_class) 51 | 52 | return ioc_class 53 | 54 | def add_class_method(self, ioc_class, name): 55 | ioc_class_method = self.create_basic_object() 56 | self.populate_class_method_reference(ioc_class_method.object.reference, name, ioc_class) 57 | 58 | self.objects.entries.append(ioc_class_method) 59 | 60 | return ioc_class_method 61 | 62 | 63 | class TestInactiveobjectsList(unittest.TestCase): 64 | 65 | @patch('sap.cli.activation.fetch_inactive_objects') 66 | def test_inactiveobjects_list(self, fake_fetch): 67 | conn = Mock() 68 | 69 | ioc_list_builder = IOCObjectListBuilder() 70 | ioc_class = ioc_list_builder.add_class('CL_PARENT_CLASS') 71 | ioc_class_method = ioc_list_builder.add_class_method(ioc_class, 'RUN') 72 | ioc_list_builder.add_class('CL_ANOTHER_CLASS') 73 | 74 | child_of_child = sap.adt.wb.IOCEntry() 75 | child_of_child.object = sap.adt.wb.IOCEntryData() 76 | child_of_child.object.reference.name = 'FAKE' 77 | child_of_child.object.reference.typ = 'SAPCLI/PYTEST' 78 | child_of_child.object.reference.parent_uri = ioc_class_method.object.uri 79 | 80 | ioc_list_builder.objects.entries.append(child_of_child) 81 | 82 | # BEGIN: malicious re-add already added objects 83 | # needed to test all statements of the function inactiveobjects_list 84 | ioc_list_builder.objects.entries.append(ioc_class) 85 | ioc_list_builder.objects.entries.append(ioc_class_method) 86 | # END 87 | 88 | fake_fetch.return_value = ioc_list_builder.objects 89 | 90 | args = INACTIVEOBJECTS_PARSER.parse('list') 91 | 92 | std_output = StringIO() 93 | err_output = StringIO() 94 | 95 | with patch_get_print_console_with_buffer() as fake_get_console: 96 | args.execute(conn, args) 97 | 98 | self.assertEqual("", fake_get_console.return_value.err_output.getvalue()) 99 | self.assertEqual(fake_get_console.return_value.std_output.getvalue(), 100 | """CL_PARENT_CLASS (CLAS/OC) 101 | + CL_PARENT_CLASS===RUN (CLAS/OC/M) 102 | CL_ANOTHER_CLASS (CLAS/OC) 103 | FAKE (SAPCLI/PYTEST) 104 | """) 105 | 106 | 107 | if __name__ == '__main__': 108 | unittest.main() 109 | -------------------------------------------------------------------------------- /test/unit/test_sap_cli_adt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from unittest.mock import MagicMock 4 | 5 | import sap.cli.adt 6 | 7 | from mock import ConsoleOutputTestCase, PatcherTestCase 8 | from infra import generate_parse_args 9 | 10 | 11 | parse_args = generate_parse_args(sap.cli.adt.CommandGroup()) 12 | 13 | 14 | class TestADT(ConsoleOutputTestCase, PatcherTestCase): 15 | 16 | def setUp(self): 17 | super(TestADT, self).setUp() 18 | 19 | assert self.console is not None 20 | 21 | self.patch_console(console=self.console) 22 | 23 | self.collection_types = { 24 | '/uri/first': ['first.v1', 'first.v2'], 25 | '/uri/second': ['second.v1', 'second.v2'] 26 | } 27 | 28 | self.adt_connection = MagicMock() 29 | self.adt_connection.collection_types = self.collection_types 30 | 31 | def tearDown(self): 32 | self.unpatch_all() 33 | 34 | def parse_collections(self): 35 | return parse_args('collections') 36 | 37 | def test_adt_without_parameters(self): 38 | args = self.parse_collections() 39 | 40 | args.execute(self.adt_connection, args) 41 | 42 | self.assertConsoleContents(self.console, stderr='', stdout='''/uri/first 43 | first.v1 44 | first.v2 45 | /uri/second 46 | second.v1 47 | second.v2 48 | ''') 49 | -------------------------------------------------------------------------------- /test/unit/test_sap_cli_datapreview.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | from unittest.mock import patch, Mock, call 5 | 6 | from argparse import ArgumentParser 7 | 8 | import sap.cli.datapreview 9 | 10 | from mock import Connection, Response 11 | from fixtures_adt_datapreview import ADT_XML_FREESTYLE_TABLE_T000_ONE_ROW 12 | 13 | 14 | parser = ArgumentParser() 15 | sap.cli.datapreview.CommandGroup().install_parser(parser) 16 | 17 | 18 | def parse_args(*argv): 19 | global parser 20 | return parser.parse_args(argv) 21 | 22 | 23 | class TestDataPreviewOSQL(unittest.TestCase): 24 | 25 | def test_print_human(self): 26 | connection = Connection([Response(text=ADT_XML_FREESTYLE_TABLE_T000_ONE_ROW, 27 | status_code=200, 28 | content_type='application/vnd.sap.adt.datapreview.table.v1+xml; charset=utf-8')]) 29 | 30 | args = parse_args('osql', 'select * from t000', '--rows', '1') 31 | with patch('sap.cli.datapreview.printout', Mock()) as fake_printout: 32 | args.execute(connection, args) 33 | 34 | self.assertEqual(fake_printout.call_args_list, [call('MANDT'), call('000')]) 35 | 36 | def test_print_human_without_headings(self): 37 | connection = Connection([Response(text=ADT_XML_FREESTYLE_TABLE_T000_ONE_ROW, 38 | status_code=200, 39 | content_type='application/vnd.sap.adt.datapreview.table.v1+xml; charset=utf-8')]) 40 | 41 | args = parse_args('osql', 'select * from t000', '-n', '--rows', '1') 42 | with patch('sap.cli.datapreview.printout', Mock()) as fake_printout: 43 | args.execute(connection, args) 44 | 45 | self.assertEqual(fake_printout.call_args_list, [call('000')]) 46 | 47 | def test_print_json(self): 48 | connection = Connection([Response(text=ADT_XML_FREESTYLE_TABLE_T000_ONE_ROW, 49 | status_code=200, 50 | content_type='application/vnd.sap.adt.datapreview.table.v1+xml; charset=utf-8')]) 51 | 52 | args = parse_args('osql', 'select * from t000', '-o', 'json', '--rows', '1') 53 | with patch('sap.cli.datapreview.printout', Mock()) as fake_printout: 54 | args.execute(connection, args) 55 | 56 | self.assertEqual(fake_printout.call_args_list, [call('''[ 57 | { 58 | "MANDT": "000" 59 | } 60 | ]''')]) 61 | -------------------------------------------------------------------------------- /test/unit/test_sap_cli_ddl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from argparse import ArgumentParser 4 | import unittest 5 | from unittest.mock import call, patch, Mock, PropertyMock 6 | from io import StringIO 7 | 8 | import sap.cli.interface 9 | import sap.cli.datadefinition 10 | 11 | from mock import patch_get_print_console_with_buffer 12 | 13 | 14 | def parse_args(argv): 15 | parser = ArgumentParser() 16 | sap.cli.datadefinition.CommandGroup().install_parser(parser) 17 | return parser.parse_args(argv) 18 | 19 | 20 | class TestCommandGroup(unittest.TestCase): 21 | 22 | def test_cli_ddl_commands_constructor(self): 23 | sap.cli.datadefinition.CommandGroup() 24 | 25 | 26 | class TestDDLActivate(unittest.TestCase): 27 | 28 | @patch('sap.adt.wb.try_activate') 29 | @patch('sap.adt.DataDefinition') 30 | def test_cli_ddl_activate_defaults(self, fake_ddl, fake_activate): 31 | instances = [] 32 | 33 | def add_instance(conn, name): 34 | ddl = Mock() 35 | ddl.name = name 36 | ddl.active = 'active' 37 | 38 | instances.append(ddl) 39 | return ddl 40 | 41 | fake_ddl.side_effect = add_instance 42 | 43 | fake_conn= Mock() 44 | 45 | fake_activate.return_value = (sap.adt.wb.CheckResults(), None) 46 | args = parse_args(['activate', 'myusers', 'mygroups']) 47 | with patch_get_print_console_with_buffer() as fake_get_console: 48 | args.execute(fake_conn, args) 49 | 50 | self.assertEqual(fake_ddl.mock_calls, [call(fake_conn, 'myusers'), call(fake_conn, 'mygroups')]) 51 | 52 | self.assertEqual(instances[0].name, 'myusers') 53 | self.assertEqual(instances[1].name, 'mygroups') 54 | 55 | self.assertEqual(fake_activate.mock_calls, [call(instances[0]), call(instances[1])]) 56 | 57 | self.assertEqual(fake_get_console.return_value.err_output.getvalue(), '') 58 | self.assertEqual(fake_get_console.return_value.std_output.getvalue(), '''Activating 2 objects: 59 | * myusers (1/2) 60 | * mygroups (2/2) 61 | Activation has finished 62 | Warnings: 0 63 | Errors: 0 64 | ''') 65 | 66 | 67 | class TestDDLRead(unittest.TestCase): 68 | 69 | @patch('sap.adt.DataDefinition') 70 | def test_cli_ddl_read_defaults(self, fake_ddl): 71 | fake_conn= Mock() 72 | fake_ddl.return_value = Mock() 73 | fake_ddl.return_value.text = 'source code' 74 | 75 | args = parse_args(['read', 'myusers']) 76 | with patch('sap.cli.datadefinition.print') as fake_print: 77 | args.execute(fake_conn, args) 78 | 79 | fake_ddl.assert_called_once_with(fake_conn, 'myusers') 80 | fake_print.assert_called_once_with('source code') 81 | 82 | 83 | if __name__ == '__main__': 84 | unittest.main() 85 | -------------------------------------------------------------------------------- /test/unit/test_sap_cli_flp.py: -------------------------------------------------------------------------------- 1 | '''FLP CLI tests.''' 2 | #!/usr/bin/env python3 3 | 4 | # pylint: disable=protected-access,missing-function-docstring 5 | 6 | import unittest 7 | import sap.cli.flp 8 | from unittest.mock import MagicMock, Mock, mock_open, patch 9 | from fixtures_flp_builder import FLP_BUILDER_CONFIG 10 | 11 | 12 | def get_sample_init_args(): 13 | args = Mock() 14 | args.config = "config" 15 | return args 16 | 17 | 18 | @patch('builtins.open', mock_open(read_data=FLP_BUILDER_CONFIG)) 19 | class TestFlpCommands(unittest.TestCase): 20 | '''Test FLP cli commands''' 21 | 22 | @patch('sap.flp.builder.Builder', autospec=True) 23 | def test_init_ok(self, builder_mock): 24 | connection = MagicMock() 25 | 26 | builder_mock.return_value.run.return_value = None 27 | 28 | sap.cli.flp.init(connection, get_sample_init_args()) 29 | 30 | builder_mock.assert_called_with(connection, "config") 31 | builder_mock.return_value.run.assert_called() 32 | -------------------------------------------------------------------------------- /test/unit/test_sap_cli_interface.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from argparse import ArgumentParser 4 | import unittest 5 | from unittest.mock import patch, mock_open 6 | from io import StringIO 7 | 8 | import sap.cli.interface 9 | 10 | from mock import Connection, Response 11 | from fixtures_adt import EMPTY_RESPONSE_OK, LOCK_RESPONSE_OK 12 | from fixtures_adt_interface import GET_INTERFACE_ADT_XML 13 | 14 | 15 | FIXTURE_ELEMENTARY_IFACE_XML=''' 16 | 17 | 18 | ''' 19 | 20 | 21 | parser = ArgumentParser() 22 | sap.cli.interface.CommandGroup().install_parser(parser) 23 | 24 | 25 | def parse_args(*argv): 26 | return parser.parse_args(argv) 27 | 28 | 29 | class TestInterfaceCreate(unittest.TestCase): 30 | 31 | def test_interface_create_defaults(self): 32 | connection = Connection([EMPTY_RESPONSE_OK]) 33 | args = parse_args('create', 'ZIF_HELLO_WORLD', 'Interface Description', '$THE_PACKAGE') 34 | args.execute(connection, args) 35 | 36 | self.assertEqual([(e.method, e.adt_uri) for e in connection.execs], [('POST', '/sap/bc/adt/oo/interfaces')]) 37 | 38 | create_request = connection.execs[0] 39 | self.assertEqual(create_request.body, bytes(FIXTURE_ELEMENTARY_IFACE_XML, 'utf-8')) 40 | 41 | self.assertIsNone(create_request.params) 42 | 43 | self.assertEqual(sorted(create_request.headers.keys()), ['Content-Type']) 44 | self.assertEqual(create_request.headers['Content-Type'], 'application/vnd.sap.adt.oo.interfaces.v5+xml; charset=utf-8') 45 | 46 | 47 | class TestInterfaceActivate(unittest.TestCase): 48 | 49 | def test_interface_activate_defaults(self): 50 | connection = Connection([ 51 | EMPTY_RESPONSE_OK, 52 | Response(text=GET_INTERFACE_ADT_XML.replace('ZIF_HELLO_WORLD', 'ZIF_ACTIVATOR'), status_code=200, headers={}) 53 | ]) 54 | 55 | args = parse_args('activate', 'ZIF_ACTIVATOR') 56 | args.execute(connection, args) 57 | 58 | self.assertEqual([(e.method, e.adt_uri) for e in connection.execs], [('POST', '/sap/bc/adt/activation'), ('GET', '/sap/bc/adt/oo/interfaces/zif_activator')]) 59 | 60 | create_request = connection.execs[0] 61 | self.assertIn('adtcore:uri="/sap/bc/adt/oo/interfaces/zif_activator"', create_request.body) 62 | self.assertIn('adtcore:name="ZIF_ACTIVATOR"', create_request.body) 63 | 64 | 65 | class TestInterfaceWrite(unittest.TestCase): 66 | 67 | def test_interface_read_from_stdin(self): 68 | args = parse_args('write', 'ZIF_WRITER', '-') 69 | 70 | conn = Connection([LOCK_RESPONSE_OK, EMPTY_RESPONSE_OK, EMPTY_RESPONSE_OK]) 71 | 72 | with patch('sys.stdin', StringIO('iface stdin definition')): 73 | args.execute(conn, args) 74 | 75 | self.assertEqual(len(conn.execs), 3) 76 | 77 | self.maxDiff = None 78 | self.assertEqual(conn.execs[1][3], b'iface stdin definition') 79 | 80 | def test_interface_read_from_file(self): 81 | conn = Connection([LOCK_RESPONSE_OK, EMPTY_RESPONSE_OK, EMPTY_RESPONSE_OK]) 82 | args = parse_args('write', 'ZIF_WRITER', 'zif_iface.abap') 83 | 84 | with patch('sap.cli.object.open', mock_open(read_data='iface file definition')) as m: 85 | args.execute(conn, args) 86 | 87 | m.assert_called_once_with('zif_iface.abap', 'r', encoding='utf8') 88 | 89 | self.assertEqual(len(conn.execs), 3) 90 | 91 | self.maxDiff = None 92 | self.assertEqual(conn.execs[1][3], b'iface file definition') 93 | 94 | 95 | if __name__ == '__main__': 96 | unittest.main() 97 | -------------------------------------------------------------------------------- /test/unit/test_sap_cli_program.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | from unittest.mock import patch, mock_open 5 | from io import StringIO 6 | from argparse import ArgumentParser 7 | 8 | import sap.cli.program 9 | 10 | from mock import Connection, Response 11 | from fixtures_adt import LOCK_RESPONSE_OK, EMPTY_RESPONSE_OK 12 | from fixtures_adt_program import GET_EXECUTABLE_PROGRAM_ADT_XML 13 | 14 | 15 | FIXTURE_STDIN_REPORT_SRC='report stdin.\n\n" Salute!\n\nwrite: \'hello, command line!\'\n' 16 | FIXTURE_FILE_REPORT_SRC='report file.\n\n" Greet!\n\nwrite: \'hello, file!\'\n' 17 | 18 | parser = ArgumentParser() 19 | sap.cli.program.CommandGroup().install_parser(parser) 20 | 21 | def parse_args(*argv): 22 | return parser.parse_args(argv) 23 | 24 | class TestProgramCreate(unittest.TestCase): 25 | 26 | def test_create_program_with_corrnr(self): 27 | connection = Connection([EMPTY_RESPONSE_OK]) 28 | 29 | args = parse_args('create', 'report', 'description', 'package', '--corrnr', '420') 30 | args.execute(connection, args) 31 | 32 | self.assertEqual(connection.execs[0].params['corrNr'], '420') 33 | 34 | 35 | class TestProgramWrite(unittest.TestCase): 36 | 37 | def test_read_from_stdin(self): 38 | conn = Connection([LOCK_RESPONSE_OK, EMPTY_RESPONSE_OK, EMPTY_RESPONSE_OK]) 39 | 40 | args = parse_args('write', 'report', '-') 41 | with patch('sys.stdin', StringIO(FIXTURE_STDIN_REPORT_SRC)): 42 | args.execute(conn, args) 43 | 44 | self.assertEqual(len(conn.execs), 3) 45 | 46 | self.maxDiff = None 47 | self.assertEqual(conn.execs[1][3], bytes(FIXTURE_STDIN_REPORT_SRC[:-1], 'utf-8')) 48 | 49 | def test_read_from_file(self): 50 | conn = Connection([LOCK_RESPONSE_OK, EMPTY_RESPONSE_OK, EMPTY_RESPONSE_OK]) 51 | 52 | args = parse_args('write', 'report', 'file.abap') 53 | with patch('sap.cli.object.open', mock_open(read_data=FIXTURE_FILE_REPORT_SRC)) as m: 54 | args.execute(conn, args) 55 | 56 | m.assert_called_once_with('file.abap', 'r', encoding='utf8') 57 | 58 | self.assertEqual(len(conn.execs), 3) 59 | 60 | self.maxDiff = None 61 | self.assertEqual(conn.execs[1][3], bytes(FIXTURE_FILE_REPORT_SRC[:-1], 'utf-8')) 62 | 63 | def test_write_with_corrnr(self): 64 | conn = Connection([LOCK_RESPONSE_OK, EMPTY_RESPONSE_OK, EMPTY_RESPONSE_OK]) 65 | 66 | args = parse_args('write', 'report', 'file.abap', '--corrnr', '420') 67 | with patch('sap.cli.object.open', mock_open(read_data=FIXTURE_FILE_REPORT_SRC)) as m: 68 | args.execute(conn, args) 69 | 70 | self.assertEqual(conn.execs[1].params['corrNr'], '420') 71 | 72 | 73 | class TestProgramActivate(unittest.TestCase): 74 | 75 | def test_activate(self): 76 | conn = Connection([ 77 | EMPTY_RESPONSE_OK, 78 | Response(text=GET_EXECUTABLE_PROGRAM_ADT_XML.replace('ZHELLO_WORLD', 'TEST_ACTIVATION'), status_code=200, headers={}) 79 | ]) 80 | 81 | args = parse_args('activate', 'test_activation') 82 | args.execute(conn, args) 83 | 84 | self.assertEqual(len(conn.execs), 2) 85 | self.assertIn('test_activation', conn.execs[0].body) 86 | 87 | 88 | if __name__ == '__main__': 89 | unittest.main() 90 | -------------------------------------------------------------------------------- /test/unit/test_sap_cli_user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | from unittest.mock import MagicMock, patch, Mock, PropertyMock, call 5 | 6 | import sap.cli.user 7 | from sap.rfc.user import today_sap_date 8 | 9 | from mock import ( 10 | ConsoleOutputTestCase, 11 | PatcherTestCase, 12 | ) 13 | 14 | from test_sap_rfc_bapi import ( 15 | create_bapiret_info 16 | ) 17 | 18 | from infra import generate_parse_args 19 | 20 | 21 | parse_args = generate_parse_args(sap.cli.user.CommandGroup()) 22 | 23 | 24 | class TestUserDetails(PatcherTestCase, ConsoleOutputTestCase): 25 | 26 | def setUp(self): 27 | super().setUp() 28 | ConsoleOutputTestCase.setUp(self) 29 | 30 | assert self.console is not None 31 | 32 | self.patch_console(console=self.console) 33 | self.conn = Mock() 34 | 35 | def test_last_login(self): 36 | self.conn.call.return_value = { 37 | 'RETURN': [], 38 | 'ALIAS': { 39 | 'USERALIAS': 'HTTP' 40 | }, 41 | 'LOGONDATA': { 42 | 'LTIME': '20200211' 43 | } 44 | } 45 | 46 | args = parse_args('details', 'ANZEIGER') 47 | args.execute(self.conn, args) 48 | 49 | self.assertConsoleContents(console=self.console, stdout='''User : ANZEIGER 50 | Alias : HTTP 51 | Last Login: 20200211 52 | ''') 53 | 54 | 55 | class TestUserCreate(PatcherTestCase, ConsoleOutputTestCase): 56 | 57 | def setUp(self): 58 | super().setUp() 59 | ConsoleOutputTestCase.setUp(self) 60 | 61 | assert self.console is not None 62 | 63 | self.patch_console(console=self.console) 64 | self.conn = Mock() 65 | 66 | def test_create_ok(self): 67 | self.conn.call.return_value = { 68 | 'RETURN': [create_bapiret_info('User created')], 69 | } 70 | 71 | args = parse_args('create', 'ANZEIGER', '--new-password', 'Victory1!') 72 | 73 | args.execute(self.conn, args) 74 | 75 | self.conn.call.assert_called_once_with('BAPI_USER_CREATE1', 76 | USERNAME='ANZEIGER', 77 | ADDRESS={'FIRSTNAME': '', 'LASTNAME': '', 'E_MAIL': ''}, 78 | PASSWORD={'BAPIPWD': 'Victory1!'}, 79 | ALIAS={'USERALIAS': ''}, 80 | LOGONDATA={'USTYP': 'Dialog', 81 | 'GLTGV': today_sap_date(), 82 | 'GLTGB': '20991231'} 83 | ) 84 | 85 | self.assertConsoleContents(console=self.console, stdout='''Success(NFO|555): User created 86 | ''') 87 | 88 | 89 | class TestUserChange(PatcherTestCase, ConsoleOutputTestCase): 90 | 91 | def setUp(self): 92 | super().setUp() 93 | ConsoleOutputTestCase.setUp(self) 94 | 95 | assert self.console is not None 96 | 97 | self.patch_console(console=self.console) 98 | self.conn = Mock() 99 | 100 | def test_change_ok(self): 101 | self.conn.call.return_value = { 102 | 'RETURN': [create_bapiret_info('User changed')], 103 | } 104 | 105 | args = parse_args('change', 'ANZEIGER', '--new-password', 'Victory1!') 106 | 107 | args.execute(self.conn, args) 108 | 109 | self.assertEqual( 110 | self.conn.call.call_args_list, 111 | [call('BAPI_USER_GET_DETAIL', USERNAME='ANZEIGER'), 112 | call('BAPI_USER_CHANGE', USERNAME='ANZEIGER', 113 | PASSWORD={'BAPIPWD': 'Victory1!'}, PASSWORDX={'BAPIPWD': 'X'})]) 114 | 115 | self.assertConsoleContents( 116 | console=self.console, 117 | stdout='''Success(NFO|555): User changed 118 | ''') 119 | -------------------------------------------------------------------------------- /test/unit/test_sap_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import unittest 5 | from unittest.mock import patch 6 | 7 | import sap.config 8 | 9 | 10 | class TestConfigGet(unittest.TestCase): 11 | 12 | def test_return_default(self): 13 | default = 'foo' 14 | actual = sap.config.config_get('bar', default=default) 15 | 16 | self.assertEqual(actual, default) 17 | 18 | def test_return_http_timeout(self): 19 | timeout = sap.config.config_get('http_timeout') 20 | 21 | self.assertEqual(timeout, 900) 22 | 23 | def test_return_http_timeout_from_env(self): 24 | with patch('os.environ', {'SAPCLI_HTTP_TIMEOUT': '0.777'}): 25 | timeout = sap.config.config_get('http_timeout') 26 | 27 | self.assertEqual(timeout, 0.777) 28 | -------------------------------------------------------------------------------- /test/unit/test_sap_errors.py: -------------------------------------------------------------------------------- 1 | '''Odata error classes tests''' 2 | #!/usr/bin/env python3 3 | 4 | # pylint: disable=missing-function-docstring 5 | 6 | import unittest 7 | 8 | from sap.errors import ResourceAlreadyExistsError 9 | 10 | class TestResourceAlreadyExistsError(unittest.TestCase): 11 | '''Test ResourceAlreadyExistsError class''' 12 | 13 | def test_str_and_repr(self): 14 | inst = ResourceAlreadyExistsError() 15 | self.assertEqual(str(inst), 'Resource already exists') 16 | -------------------------------------------------------------------------------- /test/unit/test_sap_odata_errors.py: -------------------------------------------------------------------------------- 1 | '''Odata error classes tests''' 2 | #!/usr/bin/env python3 3 | 4 | # pylint: disable=missing-function-docstring 5 | 6 | import unittest 7 | from unittest.mock import Mock 8 | 9 | from sap.odata.errors import ( 10 | HTTPRequestError, 11 | UnauthorizedError, 12 | TimedOutRequestError 13 | ) 14 | 15 | class TestOdataHTTPRequestError(unittest.TestCase): 16 | '''Test Odata HTTPRequestError class''' 17 | 18 | def test_str_and_repr(self): 19 | response = Mock() 20 | response.status_code = 'STATUS' 21 | response.text = 'TEXT' 22 | 23 | inst = HTTPRequestError('REQ', response) 24 | 25 | self.assertEqual(str(inst), 'STATUS\nTEXT') 26 | self.assertEqual(inst.response, response) 27 | self.assertEqual(inst.request, 'REQ') 28 | 29 | class TestOdataUnauthorizedError(unittest.TestCase): 30 | '''Test Odata UnauthorizedError class''' 31 | 32 | def test_str_and_repr(self): 33 | request = Mock() 34 | request.method = 'METHOD' 35 | request.url = 'URL' 36 | 37 | inst = UnauthorizedError(request, 'RESP', 'USER') 38 | 39 | self.assertEqual(str(inst), 'Authorization for the user "USER" has failed: METHOD URL') 40 | self.assertEqual(inst.response, 'RESP') 41 | 42 | class TestOdataTimedOutRequestError(unittest.TestCase): 43 | '''Test Odata TimedOutRequestError class''' 44 | 45 | def test_str_and_repr(self): 46 | request = Mock() 47 | request.method = 'METHOD' 48 | request.url = 'URL' 49 | 50 | inst = TimedOutRequestError(request, 3) 51 | 52 | self.assertEqual(str(inst), 'The request METHOD URL took more than 3s') 53 | self.assertEqual(inst.request, request) 54 | -------------------------------------------------------------------------------- /test/unit/test_sap_platform_abap_abapgit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | from io import StringIO 5 | 6 | from sap.platform.abap import Structure, ItemizedTable 7 | 8 | import sap.platform.abap.abapgit 9 | 10 | 11 | class SIMPLE_ABAP_STRUCT(Structure): 12 | 13 | FOO: str 14 | BAR: str 15 | 16 | 17 | class SIMPLE_ABAP_STRUCT_TT(ItemizedTable[SIMPLE_ABAP_STRUCT]): pass 18 | 19 | 20 | class TestXMLSerializer(unittest.TestCase): 21 | 22 | def test_full_file(self): 23 | dest = StringIO() 24 | table = sap.platform.abap.InternalTable.define('SIMPLE_TABLE', str)() 25 | table.append('FOO') 26 | table.append('BAR') 27 | 28 | writer = sap.platform.abap.abapgit.XMLWriter('LCL_PYTHON_SERIALIZER', dest) 29 | writer.add(table) 30 | writer.add(SIMPLE_ABAP_STRUCT(FOO='BAR', BAR='FOO')) 31 | writer.add(SIMPLE_ABAP_STRUCT(FOO='GRC', BAR='BLAH')) 32 | writer.add(SIMPLE_ABAP_STRUCT_TT(SIMPLE_ABAP_STRUCT(FOO='ARG', BAR='DOH'))) 33 | writer.close() 34 | 35 | self.assertEqual(dest.getvalue(), ''' 36 | 37 | 38 | 39 | 40 | FOO 41 | BAR 42 | 43 | 44 | BAR 45 | FOO 46 | 47 | 48 | GRC 49 | BLAH 50 | 51 | 52 | 53 | ARG 54 | DOH 55 | 56 | 57 | 58 | 59 | 60 | ''') 61 | 62 | 63 | class TestDOT_ABAP_GIT(unittest.TestCase): 64 | 65 | def test_dot_abap_git_from_xml(self): 66 | config = sap.platform.abap.abapgit.DOT_ABAP_GIT.from_xml(''' 67 | 68 | 69 | 70 | E 71 | /backend/ 72 | FULL 73 | 74 | /.gitignore 75 | /LICENSE 76 | /README.md 77 | /package.json 78 | /.travis.yml 79 | 80 | 81 | 82 | '''); 83 | 84 | self.assertEqual(config.MASTER_LANGUAGE, 'E') 85 | self.assertEqual(config.STARTING_FOLDER, '/backend/') 86 | self.assertEqual(config.FOLDER_LOGIC, 'FULL') 87 | self.assertEqual([itm for itm in config.IGNORE], ['/.gitignore', '/LICENSE', '/README.md', '/package.json', '/.travis.yml']) 88 | 89 | 90 | class TestAbapGitFromXml(unittest.TestCase): 91 | 92 | def test_abap_git_from_xml(self): 93 | SIMPLE_OBJECT = type('SIMPLE_OBJECT', (str,), {}) 94 | 95 | parsed = sap.platform.abap.abapgit.from_xml([SIMPLE_OBJECT, SIMPLE_ABAP_STRUCT_TT], ''' 96 | 97 | 98 | FOO 99 | 100 | 101 | ARG 102 | DOH 103 | 104 | 105 | ARG2 106 | DOH2 107 | 108 | 109 | 110 | ''') 111 | 112 | self.assertEqual(parsed['SIMPLE_OBJECT'], 'FOO') 113 | self.assertEqual(len(parsed['SIMPLE_ABAP_STRUCT_TT']), 2) 114 | self.assertEqual(parsed['SIMPLE_ABAP_STRUCT_TT'][0].FOO, 'ARG') 115 | self.assertEqual(parsed['SIMPLE_ABAP_STRUCT_TT'][0].BAR, 'DOH') 116 | self.assertEqual(parsed['SIMPLE_ABAP_STRUCT_TT'][1].FOO, 'ARG2') 117 | self.assertEqual(parsed['SIMPLE_ABAP_STRUCT_TT'][1].BAR, 'DOH2') 118 | 119 | 120 | if __name__ == '__main__': 121 | unittest.main() 122 | -------------------------------------------------------------------------------- /test/unit/test_sap_platform_language.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | from unittest.mock import patch 5 | 6 | import sap.errors 7 | import sap.platform.language 8 | 9 | 10 | class TestSAPPlatformLanguageCodes(unittest.TestCase): 11 | 12 | def test_sap_code_to_iso_code_ok(self): 13 | self.assertEqual(sap.platform.language.sap_code_to_iso_code('E'), 'EN') 14 | 15 | def test_sap_code_to_iso_code_not_found(self): 16 | with self.assertRaises(sap.errors.SAPCliError) as raised: 17 | sap.platform.language.sap_code_to_iso_code('#') 18 | 19 | self.assertEqual(str(raised.exception), 'Not found SAP Language Code: #') 20 | 21 | def test_iso_code_to_sap_code_ok(self): 22 | self.assertEqual(sap.platform.language.iso_code_to_sap_code('EN'), 'E') 23 | 24 | def test_iso_code_to_sap_code_lower_ok(self): 25 | self.assertEqual(sap.platform.language.iso_code_to_sap_code('en'), 'E') 26 | 27 | def test_iso_code_to_sap_code_not_found(self): 28 | with self.assertRaises(sap.errors.SAPCliError) as raised: 29 | sap.platform.language.iso_code_to_sap_code('#') 30 | 31 | self.assertEqual(str(raised.exception), 'Not found ISO Code: #') 32 | 33 | 34 | class TestSAPPlatformLanguageLocale(unittest.TestCase): 35 | 36 | def test_locale_lang_to_sap_code_ok(self): 37 | with patch('sap.platform.language.getlocale', return_value=('en_US', 'UTF-8')): 38 | sap_lang = sap.platform.language.locale_lang_sap_code() 39 | 40 | self.assertEqual(sap_lang, 'E') 41 | 42 | def test_locale_lang_to_sap_code_C(self): 43 | """This is a special case for a short case because C is kinda special 44 | as it is the POSIX portable locale and we may want to handle it 45 | differently in feature. 46 | """ 47 | 48 | with patch('sap.platform.language.getlocale', return_value=('C', 'UTF-8')): 49 | with self.assertRaises(sap.errors.SAPCliError) as caught: 50 | sap_lang = sap.platform.language.locale_lang_sap_code() 51 | 52 | self.assertEqual(str(caught.exception), 'The current system locale language is not ISO 3166: C') 53 | 54 | def test_locale_lang_to_sap_code_short(self): 55 | with patch('sap.platform.language.getlocale', return_value=('e', 'UTF-8')): 56 | with self.assertRaises(sap.errors.SAPCliError) as caught: 57 | sap_lang = sap.platform.language.locale_lang_sap_code() 58 | 59 | self.assertEqual(str(caught.exception), 'The current system locale language is not ISO 3166: e') 60 | 61 | def test_locale_lang_to_sap_code_unknown(self): 62 | with patch('sap.platform.language.getlocale', return_value=('WF', 'UTF-8')): 63 | with self.assertRaises(sap.errors.SAPCliError) as caught: 64 | sap_lang = sap.platform.language.locale_lang_sap_code() 65 | 66 | self.assertEqual(str(caught.exception), 'The current system locale language cannot be converted to SAP language code: WF') 67 | 68 | 69 | if __name__ == '__main__': 70 | unittest.main() 71 | -------------------------------------------------------------------------------- /test/unit/test_sap_rest_connection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from functools import partial 4 | 5 | import unittest 6 | from requests.exceptions import ConnectionError 7 | from unittest.mock import Mock, patch 8 | 9 | from sap.rest.connection import Connection 10 | from sap.rest.errors import UnauthorizedError, GCTSConnectionError 11 | 12 | 13 | def stub_retrieve(response, session, method, url, params=None, headers=None, body=None): 14 | req = Mock() 15 | req.method = method 16 | req.url = url 17 | req.params = params 18 | req.headers = headers 19 | req.body = body 20 | 21 | return (req, response) 22 | 23 | 24 | class TestConnectionExecute(unittest.TestCase): 25 | 26 | def setUp(self): 27 | icf_path = '/foo' 28 | login_path = '/bar' 29 | host = 'books.fr' 30 | client = '69' 31 | user = 'Arsan' 32 | password = 'Emmanuelle' 33 | 34 | self.conn = Connection(icf_path, login_path, host, client, user, password) 35 | 36 | @patch('sap.rest.connection.Connection._retrieve') 37 | def test_unauthorized_error(self, fake_retrieve): 38 | icf_path = '/foo' 39 | login_path = '/bar' 40 | host = 'books.fr' 41 | client = '69' 42 | user = 'Arsan' 43 | password = 'Emmanuelle' 44 | method = 'GET' 45 | url = '/all' 46 | 47 | conn = Connection(icf_path, login_path, host, client, user, password) 48 | 49 | res = Mock() 50 | res.status_code = 401 51 | fake_retrieve.side_effect = partial(stub_retrieve, res) 52 | 53 | with self.assertRaises(UnauthorizedError) as caught: 54 | conn._execute_with_session(conn._session, method, url) 55 | 56 | self.assertEqual(str(caught.exception), f'Authorization for the user "{user}" has failed: {method} {url}') 57 | 58 | @patch('sap.rest.connection.requests.Request') 59 | def test_protocol_error(self, _): 60 | session = Mock() 61 | session.send.side_effect = ConnectionError('Remote end closed connection without response') 62 | 63 | method = 'GET' 64 | url = '/all' 65 | 66 | with self.assertRaises(GCTSConnectionError) as cm: 67 | self.conn._retrieve(session, method, url) 68 | 69 | self.assertEqual(str(cm.exception), 70 | f'GCTS connection error: [HOST:"{self.conn._host}", PORT:"443", ' 71 | 'SSL:"True"] Error: Remote end closed connection without response') 72 | 73 | @patch('sap.rest.connection.requests.Request') 74 | def test_dns_error(self, _): 75 | session = Mock() 76 | session.send.side_effect = ConnectionError('[Errno -5] Dummy name resolution error') 77 | 78 | method = 'GET' 79 | url = '/all' 80 | 81 | with self.assertRaises(GCTSConnectionError) as cm: 82 | self.conn._retrieve(session, method, url) 83 | 84 | self.assertEqual(str(cm.exception), 85 | f'GCTS connection error: [HOST:"{self.conn._host}", PORT:"443", ' 86 | 'SSL:"True"] Error: Name resolution error. Check the HOST configuration.') 87 | 88 | @patch('sap.rest.connection.requests.Request') 89 | def test_connection_error(self, _): 90 | session = Mock() 91 | session.send.side_effect = ConnectionError('[Errno 111] Dummy connection error') 92 | 93 | method = 'GET' 94 | url = '/all' 95 | 96 | with self.assertRaises(GCTSConnectionError) as cm: 97 | self.conn._retrieve(session, method, url) 98 | 99 | self.assertEqual(str(cm.exception), 100 | f'GCTS connection error: [HOST:"{self.conn._host}", PORT:"443", ' 101 | 'SSL:"True"] Error: Cannot connect to the system. Check the HOST and PORT configuration.') 102 | -------------------------------------------------------------------------------- /test/unit/test_sap_rest_errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | from unittest.mock import Mock 5 | 6 | from fixtures_sap_rest_error import ( 7 | GCTS_RESPONSE_FORBIDDEN, 8 | GCTS_RESPONSE_FORBIDDEN_NO_ERROR_HEADER, 9 | GCTS_RESPONSE_FORBIDDEN_NO_ERROR_MESSAGE, 10 | ) 11 | 12 | from sap.rest.errors import ( 13 | UnauthorizedError, 14 | TimedOutRequestError, 15 | HTTPRequestError, 16 | ) 17 | 18 | 19 | class TestUnauthorizedError(unittest.TestCase): 20 | 21 | def test_str_and_repr(self): 22 | request = Mock() 23 | request.method = "GET" 24 | request.url = "http://example.com" 25 | response = Mock() 26 | user = "anzeiger" 27 | 28 | inst = UnauthorizedError(request, response, user) 29 | 30 | self.assertEqual(str(inst), f'Authorization for the user "{user}" has failed: {request.method} {request.url}') 31 | self.assertEqual(inst.response, response) 32 | 33 | 34 | class TestTimeoutError(unittest.TestCase): 35 | 36 | def test_str_and_repr(self): 37 | request = Mock() 38 | request.method = "GET" 39 | request.url = "http://example.com" 40 | timeout = float(10) 41 | 42 | inst = TimedOutRequestError(request, timeout) 43 | 44 | self.assertEqual(str(inst), f'The request {request.method} {request.url} took more than {timeout}s') 45 | 46 | 47 | class TestHttpRequestError(unittest.TestCase): 48 | 49 | def test_str_and_repr_with_match(self): 50 | response = GCTS_RESPONSE_FORBIDDEN 51 | 52 | inst = HTTPRequestError('Request', response) 53 | 54 | self.assertEqual(str(inst), '403 Forbidden\n' 55 | 'The request has been blocked by UCON.\n' 56 | 'And the multiline error message.') 57 | 58 | def test_str_and_repr_no_error_header(self): 59 | response = GCTS_RESPONSE_FORBIDDEN_NO_ERROR_HEADER 60 | 61 | inst = HTTPRequestError('Request', response) 62 | 63 | self.assertEqual(str(inst), f'{response.status_code}\n{response.text}') 64 | 65 | def test_str_and_repr_no_error_message(self): 66 | response = GCTS_RESPONSE_FORBIDDEN_NO_ERROR_MESSAGE 67 | 68 | inst = HTTPRequestError('Request', response) 69 | 70 | self.assertEqual(str(inst), f'{response.status_code}\n{response.text}') 71 | -------------------------------------------------------------------------------- /test/unit/test_sap_rfc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | from unittest.mock import patch, MagicMock, Mock 5 | 6 | import sap.errors 7 | import sap.rfc.core 8 | 9 | class TestRFCCore(unittest.TestCase): 10 | 11 | def tearDown(self): 12 | sap.rfc.core._unimport_pyrfc() 13 | 14 | def test_rfc_is_available_no(self): 15 | with patch('importlib.__import__') as fake_import: 16 | fake_import.side_effect = ImportError('mock error') 17 | 18 | self.assertFalse(sap.rfc.core.rfc_is_available()) 19 | 20 | def test_rfc_is_available_yes(self): 21 | fake_pyrfc = MagicMock() 22 | 23 | with patch.dict('sys.modules', pyrfc=fake_pyrfc): 24 | self.assertTrue(sap.rfc.core.rfc_is_available()) 25 | 26 | # Make sure it does not try to import it again 27 | with patch('importlib.__import__') as fake_import: 28 | fake_import.side_effect = ImportError('mock error') 29 | 30 | self.assertTrue(sap.rfc.core.rfc_is_available()) 31 | 32 | def test_connect_non_available(self): 33 | with self.assertRaises(sap.errors.SAPCliError) as caught: 34 | with patch('importlib.__import__') as fake_import: 35 | fake_import.side_effect = ImportError('mock error') 36 | 37 | sap.rfc.core.connect(ashost='ashost', 38 | sysnr='00', 39 | client='100', 40 | user='anzeiger', 41 | password='display') 42 | 43 | self.assertEqual(str(caught.exception), 44 | 'RFC functionality is not available(enabled)') 45 | 46 | def test_connect_available(self): 47 | fake_pyrfc = MagicMock() 48 | 49 | with patch.dict('sys.modules', pyrfc=fake_pyrfc): 50 | conn = sap.rfc.core.connect(ashost='ashost', 51 | sysnr='00', 52 | client='100', 53 | user='anzeiger', 54 | passwd='display') 55 | 56 | self.assertIsNotNone(conn) 57 | self.assertEqual(conn, fake_pyrfc.Connection.return_value) 58 | fake_pyrfc.Connection.assert_called_once_with(ashost='ashost', 59 | sysnr='00', 60 | client='100', 61 | user='anzeiger', 62 | passwd='display') 63 | -------------------------------------------------------------------------------- /test/unit/test_sap_rfc_core.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import unittest 3 | from unittest.mock import patch, Mock 4 | 5 | import sap.rfc.core 6 | from sap.rfc.errors import RFCLoginError, RFCCommunicationError, SAPCliError 7 | 8 | 9 | class TestTryPyRFCExceptionType(unittest.TestCase): 10 | 11 | @patch('sap.rfc.core.rfc_is_available', return_value=False) 12 | def test_pyrfc_is_not_available(self, _): 13 | typ = None 14 | with self.assertRaises(SAPCliError) as caught: 15 | typ = sap.rfc.core.try_pyrfc_exception_type() 16 | 17 | self.assertIsNone(typ) 18 | self.assertEqual(str(caught.exception), 'RFC functionality is not available(enabled)') 19 | 20 | def test_pyrfc_logon_error(self): 21 | class FakeLogonError(Exception): 22 | def __init__(self, msg): 23 | self.message = msg 24 | 25 | fake_saprfc_module = Mock() 26 | fake_saprfc_module._exception.LogonError = FakeLogonError 27 | fake_saprfc_module.Connection.side_effect = fake_saprfc_module._exception.LogonError('Bad login') 28 | 29 | patch('sap.rfc.core.SAPRFC_MODULE', new=fake_saprfc_module).start() 30 | 31 | with self.assertRaises(RFCLoginError) as cm: 32 | sap.rfc.core.connect(ashost='host', user='user') 33 | print('the exception is not raised') 34 | 35 | self.assertEqual(str(cm.exception), 'RFC connection error: [HOST:"host", USER:"user"] Error: Bad login') 36 | 37 | def test_pyrfc_communication_error(self): 38 | class FakeLogonError(Exception): 39 | pass 40 | 41 | class FakeCommunicationError(Exception): 42 | def __init__(self, msg): 43 | self.message = msg 44 | 45 | fake_saprfc_module = Mock() 46 | # if not defined python throws TypeError because Mock does not derive from Exception 47 | fake_saprfc_module._exception.LogonError = FakeLogonError 48 | 49 | fake_saprfc_module._exception.CommunicationError = FakeCommunicationError 50 | fake_saprfc_module.Connection.side_effect = fake_saprfc_module._exception.CommunicationError('ERROR Communication error') 51 | 52 | patch('sap.rfc.core.SAPRFC_MODULE', new=fake_saprfc_module).start() 53 | 54 | with self.assertRaises(RFCCommunicationError) as cm: 55 | sap.rfc.core.connect(ashost='host', user='user') 56 | 57 | self.assertEqual(str(cm.exception), 'RFC connection error: [HOST:"host", USER:"user"] Error: Communication error') 58 | -------------------------------------------------------------------------------- /test/unit/test_sap_rfc_strust_exceptions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from sap.rfc.strust import UploadCertError, InvalidSSLStorage, PutCertificateError 4 | 5 | 6 | class TestUploadException(unittest.TestCase): 7 | 8 | def test_ctor_with_message(self): 9 | ex = UploadCertError('Something has failed') 10 | self.assertEqual(str(ex), 'Something has failed') 11 | 12 | 13 | class TestInvalidSSLStorage(unittest.TestCase): 14 | 15 | def test_ctor_with_message(self): 16 | ex = InvalidSSLStorage('Something has failed') 17 | self.assertEqual(str(ex), 'Something has failed') 18 | 19 | 20 | class TestPutCertificateError(unittest.TestCase): 21 | 22 | def test_ctor_with_message(self): 23 | ex = PutCertificateError('Something has failed') 24 | self.assertEqual(str(ex), 'Something has failed') 25 | 26 | 27 | if __name__ == '__main__': 28 | unittest.main() 29 | --------------------------------------------------------------------------------