├── MANIFEST.in ├── doc ├── _static │ ├── README │ └── images │ │ ├── SNC0-1.png │ │ ├── SNC0-2.png │ │ ├── SU01-SNC.png │ │ ├── EXTID_DN-1.png │ │ ├── EXTID_DN-2.png │ │ ├── SMP-SAPCAR.png │ │ ├── exceptions.dia │ │ ├── exceptions.png │ │ ├── SMP-SAPNWRFCSDK.png │ │ ├── exceptions_v1-8-2.dia │ │ ├── exceptions_v1-8-2.png │ │ ├── server │ │ ├── sm59-serializer.png │ │ ├── z_nwrfc_server_bgrfc.png │ │ ├── SBGRFCMON1-Client-MME.png │ │ ├── SBGRFCMON2-Client-MME.png │ │ ├── SBGRFCCONF-Scheduler-App-Server.png │ │ ├── SBGRFCCONF-Scheduler-Destination.png │ │ └── SBGRFCCONF-Define-Inbound-Destination.png │ │ └── rfm.svg ├── tables │ ├── data_types.ods │ ├── error_types.ods │ ├── server_error.ods │ ├── error_types.csv │ ├── data_types.csv │ └── server_error.csv ├── glossary.rst ├── bibliography.rst ├── index.rst ├── remarks.rst ├── build.rst ├── intro.rst ├── authentication.rst ├── pyrfc.rst ├── install.rst └── conf.py ├── examples ├── server │ ├── doc │ │ ├── sm59-serializer.png │ │ ├── SBGRFCMON-Queue-saved.png │ │ ├── SBGRFCMON1-Client-MME.png │ │ ├── SBGRFCMON2-Client-MME.png │ │ ├── z_nwrfc_server_bgrfc.png │ │ ├── SBGRFCMON-release-queue.png │ │ ├── SBGRFCCONF-Scheduler-App-Server.png │ │ ├── SBGRFCCONF-Scheduler-Destination.png │ │ └── SBGRFCCONF-Define-Inbound-Destination.png │ ├── error.py │ ├── bgrfc_unit_state.py │ ├── backends.py │ ├── bgrfc_client.py │ ├── z_stfc_connection_call.abap │ ├── z_stfc_structure_call.abap │ ├── server_errors.py │ ├── server_app_thread.py │ ├── server_pyrfc_thread.py │ ├── README.md │ ├── trfc_test.py │ ├── tlog.py │ ├── z_nwrfc_server_bgrfc.abap │ └── bgrfc_server.py ├── __init__.py ├── timeout │ ├── timeout_connection.py │ └── timeout_call.py ├── serverStfcConnection.py ├── systest.py ├── config.py ├── clientStfcStructure.py ├── serverFunctionDescription.py ├── clientPrintDescription.py └── clientIDoc.py ├── tests ├── __init__.py ├── data │ └── __init__.py ├── pyrfc.cfg ├── server │ ├── client.py │ └── server.py ├── test_rfcsdk.py ├── test_table_type.py ├── test_wsrfc.py ├── test_options.py ├── test_package.py ├── test_function_group_mrfc.py ├── test_timeout.py ├── function_description_utils.py ├── test_errors.py ├── test_function_group_stfc.py ├── test_client_config.py ├── test_throughput.py ├── config.py ├── test_errors_abap.py ├── test_server.py └── test_connection.py ├── src └── pyrfc │ ├── _utils.py │ ├── __init__.py │ └── _exception.py ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .reuse └── dep5 ├── pyproject.toml ├── tox.ini ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── setup.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-exclude tests * 2 | include src/pyrfc/csapnwrfc.pxd 3 | -------------------------------------------------------------------------------- /doc/_static/README: -------------------------------------------------------------------------------- 1 | Place for static resources used for the Sphinx documentation 2 | -------------------------------------------------------------------------------- /doc/tables/data_types.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/tables/data_types.ods -------------------------------------------------------------------------------- /doc/tables/error_types.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/tables/error_types.ods -------------------------------------------------------------------------------- /doc/tables/server_error.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/tables/server_error.ods -------------------------------------------------------------------------------- /doc/_static/images/SNC0-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/SNC0-1.png -------------------------------------------------------------------------------- /doc/_static/images/SNC0-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/SNC0-2.png -------------------------------------------------------------------------------- /doc/_static/images/SU01-SNC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/SU01-SNC.png -------------------------------------------------------------------------------- /doc/_static/images/EXTID_DN-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/EXTID_DN-1.png -------------------------------------------------------------------------------- /doc/_static/images/EXTID_DN-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/EXTID_DN-2.png -------------------------------------------------------------------------------- /doc/_static/images/SMP-SAPCAR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/SMP-SAPCAR.png -------------------------------------------------------------------------------- /doc/_static/images/exceptions.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/exceptions.dia -------------------------------------------------------------------------------- /doc/_static/images/exceptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/exceptions.png -------------------------------------------------------------------------------- /doc/_static/images/SMP-SAPNWRFCSDK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/SMP-SAPNWRFCSDK.png -------------------------------------------------------------------------------- /doc/_static/images/exceptions_v1-8-2.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/exceptions_v1-8-2.dia -------------------------------------------------------------------------------- /doc/_static/images/exceptions_v1-8-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/exceptions_v1-8-2.png -------------------------------------------------------------------------------- /examples/server/doc/sm59-serializer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/examples/server/doc/sm59-serializer.png -------------------------------------------------------------------------------- /doc/_static/images/server/sm59-serializer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/server/sm59-serializer.png -------------------------------------------------------------------------------- /examples/server/doc/SBGRFCMON-Queue-saved.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/examples/server/doc/SBGRFCMON-Queue-saved.png -------------------------------------------------------------------------------- /examples/server/doc/SBGRFCMON1-Client-MME.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/examples/server/doc/SBGRFCMON1-Client-MME.png -------------------------------------------------------------------------------- /examples/server/doc/SBGRFCMON2-Client-MME.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/examples/server/doc/SBGRFCMON2-Client-MME.png -------------------------------------------------------------------------------- /examples/server/doc/z_nwrfc_server_bgrfc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/examples/server/doc/z_nwrfc_server_bgrfc.png -------------------------------------------------------------------------------- /examples/server/doc/SBGRFCMON-release-queue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/examples/server/doc/SBGRFCMON-release-queue.png -------------------------------------------------------------------------------- /doc/_static/images/server/z_nwrfc_server_bgrfc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/server/z_nwrfc_server_bgrfc.png -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | -------------------------------------------------------------------------------- /doc/_static/images/server/SBGRFCMON1-Client-MME.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/server/SBGRFCMON1-Client-MME.png -------------------------------------------------------------------------------- /doc/_static/images/server/SBGRFCMON2-Client-MME.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/server/SBGRFCMON2-Client-MME.png -------------------------------------------------------------------------------- /tests/data/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | -------------------------------------------------------------------------------- /examples/server/doc/SBGRFCCONF-Scheduler-App-Server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/examples/server/doc/SBGRFCCONF-Scheduler-App-Server.png -------------------------------------------------------------------------------- /examples/server/doc/SBGRFCCONF-Scheduler-Destination.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/examples/server/doc/SBGRFCCONF-Scheduler-Destination.png -------------------------------------------------------------------------------- /doc/_static/images/server/SBGRFCCONF-Scheduler-App-Server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/server/SBGRFCCONF-Scheduler-App-Server.png -------------------------------------------------------------------------------- /doc/_static/images/server/SBGRFCCONF-Scheduler-Destination.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/server/SBGRFCCONF-Scheduler-Destination.png -------------------------------------------------------------------------------- /examples/server/doc/SBGRFCCONF-Define-Inbound-Destination.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/examples/server/doc/SBGRFCCONF-Define-Inbound-Destination.png -------------------------------------------------------------------------------- /doc/_static/images/server/SBGRFCCONF-Define-Inbound-Destination.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/PyRFC/HEAD/doc/_static/images/server/SBGRFCCONF-Define-Inbound-Destination.png -------------------------------------------------------------------------------- /tests/pyrfc.cfg: -------------------------------------------------------------------------------- 1 | [coevi51] 2 | user = demo 3 | passwd = welcome 4 | ashost = 10.68.110.51 5 | sysnr = 00 6 | client = 620 7 | lang = EN 8 | 9 | [coevi51dest] 10 | dest = MME 11 | 12 | -------------------------------------------------------------------------------- /doc/glossary.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. _glossary: 4 | 5 | ======== 6 | Glossary 7 | ======== 8 | 9 | .. glossary:: 10 | :sorted: 11 | 12 | test 13 | This is a test entry 14 | -------------------------------------------------------------------------------- /tests/server/client.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | from pyrfc import Connection 6 | 7 | with Connection(dest="MME") as client: 8 | print(client.get_function_description("BAPI_USER_GET_DETAIL")) 9 | -------------------------------------------------------------------------------- /src/pyrfc/_utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | 6 | def enum_names(enum_obj): 7 | """Enum object names.""" 8 | return {en.name for en in enum_obj} 9 | 10 | 11 | def enum_values(enum_obj): 12 | """Enum object values.""" 13 | return {en.value for en in enum_obj} 14 | -------------------------------------------------------------------------------- /examples/server/error.py: -------------------------------------------------------------------------------- 1 | from pyrfc import ABAPRuntimeError 2 | 3 | errorInfo = { 4 | "code": 4, 5 | "key": "Function not supported", 6 | "message": "", 7 | "msg_class": "SR", 8 | "msg_type": "A", 9 | "msg_number": "006", 10 | "msg_v1": "", 11 | "msg_v2": "", 12 | "msg_v3": "", 13 | "msg_v4": "", 14 | } 15 | 16 | err = ABAPRuntimeError(**errorInfo) 17 | 18 | print(err) 19 | -------------------------------------------------------------------------------- /examples/server/bgrfc_unit_state.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import sys 6 | 7 | from pyrfc import Connection 8 | 9 | with Connection(dest=sys.argv[1]) as client: 10 | print( 11 | client.get_unit_state({ 12 | "id": sys.argv[2], 13 | "background": True, 14 | "queued": True, 15 | }) 16 | ) 17 | 18 | client.close() 19 | -------------------------------------------------------------------------------- /examples/server/backends.py: -------------------------------------------------------------------------------- 1 | BACKEND = { 2 | "ALX": [ 3 | {"dest": "ALX_GATEWAY"}, 4 | {"dest": "ALX"}, 5 | { 6 | "port": 8081, 7 | "server_log": True, 8 | }, 9 | ], 10 | "QM7": [ 11 | {"dest": "gatewayqm7"}, 12 | {"dest": "QM7"}, 13 | { 14 | "port": 8081, 15 | "server_log": False, 16 | }, 17 | ], 18 | "MME": [ 19 | {"dest": "gateway"}, 20 | {"dest": "MME"}, 21 | { 22 | "port": 8081, 23 | "server_log": True, 24 | }, 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Describe the steps to reproduce the behavior, including test script. 15 | 16 | **Screenshots** 17 | If applicable, add screenshots to help explain your problem. 18 | 19 | **Environment** 20 | - OS: Windows, Darwin or Linux 21 | - Running in docker? 22 | - PyRFC version 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /examples/timeout/timeout_connection.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import sys 6 | 7 | from pyrfc import Connection, RFCError 8 | 9 | client = Connection(dest=sys.argv[1], config={"timeout": 5}) 10 | 11 | try: 12 | # 10 seconds long RFC call cancelled after 5 seconds 13 | # because of timeout set at connection level, for all RFC calls 14 | client.call( 15 | "RFC_PING_AND_WAIT", 16 | SECONDS=10, 17 | ) 18 | except RFCError as ex: 19 | print(ex.code, ex.key, ex.message) 20 | # 7 RFC_CANCELED Connection was canceled: 5067486720. New handle: 4806705664 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /examples/timeout/timeout_call.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import sys 6 | 7 | from pyrfc import Connection, RFCError 8 | 9 | client = Connection(dest=sys.argv[1], config={"timeout": 20}) 10 | 11 | try: 12 | # 10 seconds long RFC call, cancelled after 5 seconds, 13 | # despite 15 seconds timeout at connection level 14 | client.call( 15 | "RFC_PING_AND_WAIT", 16 | options={"timeout": 5}, 17 | SECONDS=10, 18 | ) 19 | except RFCError as ex: 20 | print(ex.code, ex.key, ex.message) 21 | # 7 RFC_CANCELED Connection was canceled: 5059097088. New handle: 4798322688 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | *.iml 4 | .vscode 5 | *.code-workspace 6 | 7 | *.py[cod] 8 | __pycache__/ 9 | *.so 10 | *.o 11 | *.trc 12 | *.log 13 | *.c 14 | *.swp 15 | *.html 16 | *.inv 17 | sapnwrfc.ini 18 | 19 | *.egg 20 | *.eggs 21 | *.egg-info 22 | .*cache*/ 23 | *venv*/ 24 | _sources 25 | 26 | build/ 27 | dist/ 28 | develop-eggs/ 29 | doc/_build/ 30 | 31 | pyrfc.cfg 32 | setuptools*.egg 33 | .pytest_cache 34 | .python-version 35 | 36 | ci/test 37 | ci/issues 38 | 39 | .tox 40 | # pytest-html-reporter 41 | report 42 | archive 43 | output.json 44 | 45 | .coverage 46 | coverage*.* 47 | 48 | # cpp source removed from git 49 | src/pyrfc/_cyrfc.cpp 50 | tmp 51 | -------------------------------------------------------------------------------- /examples/server/bgrfc_client.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import sys 6 | 7 | from pyrfc import Connection 8 | 9 | client = Connection(dest=sys.argv[1]) 10 | 11 | unit = client.initialize_unit() 12 | 13 | name = "BGRFC_TEST_OUTIN" 14 | counter = "00001" 15 | 16 | client.fill_and_submit_unit( 17 | unit, 18 | [("STFC_WRITE_TO_TCPIC", {"TCPICDAT": [f"{name:20}{counter:20}{unit['id']:32}"]})], 19 | queue_names=["RFCSDK_QUEUE_IN"], 20 | attributes={"lock": 1}, 21 | ) 22 | 23 | print(unit, client.get_unit_state(unit)) 24 | 25 | input("Press Enter ...\n") 26 | 27 | print(unit, client.get_unit_state(unit)) 28 | 29 | client.close() 30 | -------------------------------------------------------------------------------- /tests/test_rfcsdk.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | from pyrfc import language_iso_to_sap, language_sap_to_iso, reload_ini_file 6 | 7 | from tests.config import LANGUAGES 8 | 9 | 10 | class TestRfcSDK: 11 | def test_reload_ini_file(self): 12 | """sapnwrfc.ini file reload test.""" 13 | 14 | reload_ini_file() 15 | # no errors expected 16 | assert True 17 | 18 | def test_language_iso_sap(self): 19 | """SAP language code to ISO language code conversion and vice versa.""" 20 | 21 | for lang_iso in LANGUAGES: 22 | lang_sap = language_iso_to_sap(lang_iso) 23 | assert lang_sap == LANGUAGES[lang_iso]["lang_sap"] 24 | laiso = language_sap_to_iso(lang_sap) 25 | assert laiso == lang_iso 26 | -------------------------------------------------------------------------------- /doc/tables/error_types.csv: -------------------------------------------------------------------------------- 1 | ======================= =============================== =========================== ==================== 2 | type (SPJ) code [numeric] (C) group (C) class (Python) 3 | ======================= =============================== =========================== ==================== 4 | ABAP exception RFC_ABAP_EXCEPTION [5] ABAP_APPLICATION_FAILURE ABAPApplicationError 5 | system failure RFC_ABAP_RUNTIME_FAILURE [3] ABAP_RUNTIME_FAILURE ABAPRuntimeError 6 | ABAP message RFC_ABAP_MESSAGE [4] ABAP_RUNTIME_FAILURE ABAPRuntimeError 7 | communication failure RFC_COMMUNICATION_FAILURE [1] COMMUNICATION_FAILURE CommunicationError 8 | - RFC_LOGON_FAILURE [2] LOGON_FAILURE LogonError 9 | ======================= =============================== =========================== ==================== 10 | -------------------------------------------------------------------------------- /examples/serverStfcConnection.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import os 6 | 7 | from pyrfc import Server, set_ini_file_directory 8 | 9 | 10 | def my_stfc_connection( 11 | request_context=None, 12 | REQUTEXT="", 13 | ): 14 | print("stfc invoked") 15 | print( 16 | "request_context", 17 | request_context, 18 | ) 19 | print(f"REQUTEXT: {REQUTEXT}") 20 | return { 21 | "ECHOTEXT": REQUTEXT, 22 | "RESPTEXT": "Python server here", 23 | } 24 | 25 | 26 | dir_path = os.path.dirname(os.path.realpath(__file__)) 27 | set_ini_file_directory(dir_path) 28 | 29 | server = Server( 30 | {"dest": "gateway"}, 31 | {"dest": "MME"}, 32 | ) 33 | 34 | server.add_function( 35 | "STFC_CONNECTION", 36 | my_stfc_connection, 37 | ) 38 | 39 | print("\nPress CTRL-C to skip server test...") 40 | server.serve(20) 41 | -------------------------------------------------------------------------------- /examples/server/z_stfc_connection_call.abap: -------------------------------------------------------------------------------- 1 | *&---------------------------------------------------------------------* 2 | *& Report ZSERVERTEST 3 | *&---------------------------------------------------------------------* 4 | *& MME/620 5 | *&---------------------------------------------------------------------* 6 | report zservertest. 7 | 8 | data lv_echo like sy-lisel. 9 | data lv_resp like sy-lisel. 10 | data lv_error_message type char512. 11 | 12 | do 10 times. 13 | 14 | call function 'STFC_CONNECTION' destination 'NODEJS' 15 | exporting 16 | requtext = 'XYZ' 17 | importing 18 | echotext = lv_echo 19 | resptext = lv_resp 20 | exceptions 21 | communication_failure = 1 message lv_error_message 22 | system_failure = 2 message lv_error_message. 23 | 24 | if sy-subrc = 0. 25 | write lv_echo. 26 | write lv_resp. 27 | else. 28 | write lv_error_message. 29 | exit. 30 | endif. 31 | enddo. 32 | -------------------------------------------------------------------------------- /examples/systest.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import sysconfig 6 | 7 | print("From sysconfig:") 8 | for key in ( 9 | "Py_DEBUG", 10 | "WITH_PYMALLOC", 11 | "Py_UNICODE_SIZE", 12 | ): 13 | try: 14 | print(key + ": " + repr(sysconfig.get_config_var(key))) 15 | except Exception as ex: 16 | print( 17 | "Error getting %s" % key, 18 | ex, 19 | ) 20 | 21 | print("From headers:") 22 | h_file = sysconfig.get_config_h_filename() 23 | with open( 24 | h_file, 25 | "r", 26 | ) as file: 27 | conf_vars = sysconfig.parse_config_h(file) 28 | for key in ( 29 | "Py_DEBUG", 30 | "WITH_PYMALLOC", 31 | "Py_UNICODE_SIZE", 32 | ): 33 | try: 34 | print(key + ": " + repr(conf_vars[key])) 35 | except Exception as ex: 36 | print( 37 | "Error getting %s" % key, 38 | ex, 39 | ) 40 | -------------------------------------------------------------------------------- /doc/bibliography.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Bibliography 3 | ============ 4 | 5 | 6 | .. _c09a: 7 | 8 | Schmidt and Li (2009a) 9 | ---------------------- 10 | Improve communication between your C/C++ applications and SAP systems with SAP NetWeaver RFC SDK - 11 | `Part 1: RFC Client Programming `_, 12 | *SAP Professional Journal*, pp 1-16 (originally published in November 2007) 13 | 14 | .. _c09b: 15 | 16 | Schmidt and Li (2009b) 17 | ---------------------- 18 | Improve communication between your C/C++ applications and SAP systems with SAP NetWeaver RFC SDK - 19 | - `Part 2: RFC Server Programming `_, 20 | *SAP Professional Journal*, pp 1-13 (originally published in January/February 2008) 21 | 22 | .. _c09c: 23 | 24 | Schmidt and Li (2009c) 25 | ---------------------- 26 | Improve communication between your C/C++ applications and SAP systems with SAP NetWeaver RFC SDK - 27 | `Part 3: Advanced Topics `_, 28 | *SAP Professional Journal*, pp 1-18 (originally published in March 2008) 29 | -------------------------------------------------------------------------------- /doc/tables/data_types.csv: -------------------------------------------------------------------------------- 1 | Type Category ABAP Meaning RFC Python Remark 2 | numeric I Integer (whole number) INT int Internal 1 and 2 byte integers (INT1, INT2) are also mapped to int 3 | numeric F Floating point number FLOAT float 4 | numeric P Packed number / BCD number BCD Decimal 5 | character C Text field (alphanumeric characters) CHAR unicode 6 | character D Date field (Format: YYYYMMDD) DATE datetime.date 7 | character T Time field (Format: HHMMSS) TIME datetime.time 8 | character N Numeric text field (numeric characters) NUM unicode 9 | hexadecimal X Hexadecimal field BYTE str [bytes] 10 | Variable Length STRING Dynamic length string STRING unicode 11 | Variable Length XSTRING Dynamic length hexadecimal string BYTE str [bytes] 12 | -------------------------------------------------------------------------------- /tests/test_table_type.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | from pyrfc import Connection 6 | 7 | from tests.config import CONNECTION_DEST as params 8 | 9 | 10 | class TestTableTypeParameters: 11 | """ 12 | This test cases cover table types of variable and structure types 13 | """ 14 | 15 | def setup_method(self): 16 | self.conn = Connection(**params) 17 | assert self.conn.alive 18 | 19 | def teardown_method(self): 20 | self.conn.close() 21 | assert not self.conn.alive 22 | 23 | def test_table_type_parameter(self): 24 | res = self.conn.call( 25 | "/COE/RBP_PAM_SERVICE_ORD_CHANG", 26 | IV_ORDERID="4711", 27 | IT_NOTICE_NOTIFICATION=[ 28 | {"": "ABCD"}, 29 | {"": "XYZ"}, 30 | ], 31 | ) 32 | assert len(res["ET_RETURN"]) > 0 33 | erl = res["ET_RETURN"][0] 34 | assert erl["TYPE"] == "E" 35 | assert erl["ID"] == "IWO_BAPI" 36 | assert erl["NUMBER"] == "121" 37 | assert erl["MESSAGE_V1"] == "4711" 38 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | PyRFC - The Python RFC Connector 2 | ================================ 3 | 4 | The `pyrfc Python package `_ provides Python bindings for *SAP NetWeaver RFC Library*, 5 | for a comfortable way of calling ABAP modules from Python and Python modules from ABAP, 6 | via SAP Remote Function Call (RFC) protocol. 7 | 8 | It was inspired by `Piers Harding's sapnwrfc package `_, 9 | wrapping the existing *SAP NetWeaver RFC Library* and rewritten using *Cython*. 10 | 11 | To start using :mod:`pyrfc` follow the :ref:`Installation ` guide. 12 | 13 | 14 | Documentation 15 | ============= 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | 20 | intro 21 | install 22 | client 23 | server 24 | authentication 25 | build 26 | remarks 27 | bibliography 28 | 29 | API documentation 30 | ================= 31 | 32 | .. toctree:: 33 | :maxdepth: 2 34 | 35 | pyrfc 36 | 37 | Indices and tables 38 | ================== 39 | 40 | .. Glossary excluded. 41 | * :ref:`glossary` 42 | * :ref:`modindex` 43 | 44 | * :ref:`genindex` 45 | * :ref:`search` 46 | -------------------------------------------------------------------------------- /examples/server/z_stfc_structure_call.abap: -------------------------------------------------------------------------------- 1 | *&---------------------------------------------------------------------* 2 | *& Report ZSERVERTEST 3 | *&---------------------------------------------------------------------* 4 | *& QM7/005 5 | *&---------------------------------------------------------------------* 6 | report zservertest. 7 | 8 | data ls_struct like rfctest. 9 | data lt_table like table of rfctest. 10 | data lv_resp like sy-lisel. 11 | data lv_error_message type char512. 12 | 13 | do 10 times. 14 | add 1 to : ls_struct-RFCINT1, ls_struct-RFCINT2, ls_struct-RFCINT4. 15 | insert ls_struct into table lt_table. 16 | call function 'STFC_STRUCTURE' destination 'NODEJS' 17 | exporting 18 | importstruct = ls_struct 19 | importing 20 | echostruct = ls_struct 21 | resptext = lv_resp 22 | tables 23 | rfctable = lt_table 24 | exceptions 25 | communication_failure = 1 message lv_error_message 26 | system_failure = 2 message lv_error_message 27 | . 28 | 29 | if sy-subrc = 0. 30 | write lv_resp. 31 | else. 32 | write lv_error_message. 33 | exit. 34 | endif. 35 | 36 | enddo. 37 | -------------------------------------------------------------------------------- /examples/config.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | from configparser import ConfigParser 6 | from os import path 7 | 8 | cp = ConfigParser() 9 | cp.read( 10 | path.join( 11 | path.dirname(path.abspath(__file__)), 12 | "pyrfc.cfg", 13 | ) 14 | ) 15 | 16 | sections = cp.sections() 17 | print(sections) 18 | # ['p7019s16', 'coevi51', 'coe_he_66', 'gateway', 'connection_e1q'] 19 | coevi51_params = dict(cp.items("coevi51")) 20 | print(coevi51_params) 21 | 22 | 23 | def get_error( 24 | ex, 25 | ): 26 | error = {} 27 | ex_type_full = str(type(ex)) 28 | print(ex_type_full) 29 | # error["type"] = ex_type_full[ex_type_full.rfind(".") + 1 : ex_type_full.rfind("'")] # noqa: E501 30 | error["code"] = ex.code if "code" in ex else "" 31 | error["key"] = ex.key if "key" in ex else "" 32 | error["message"] = ex.message.split("\n") 33 | error["msg_class"] = ex.msg_class if "msg_class" in ex else "" 34 | error["msg_type"] = ex.msg_type if "msg_type" in ex else "" 35 | error["msg_number"] = ex.msg_number if "msg_number" in ex else "" 36 | error["msg_v1"] = ex.msg_v1 if "msg_v1" in ex else "" 37 | return error 38 | -------------------------------------------------------------------------------- /examples/server/server_errors.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import sys 6 | 7 | from backends import BACKEND 8 | from pyrfc import Server 9 | 10 | backend_dest = sys.argv[1] 11 | 12 | errorInfo = { 13 | "code": 4, 14 | "key": "Function not supported", 15 | "message": "", 16 | "msg_class": "SR", 17 | "msg_type": "A", 18 | "msg_number": "006", 19 | "msg_v1": "", 20 | "msg_v2": "", 21 | "msg_v3": "", 22 | "msg_v4": "", 23 | } 24 | 25 | 26 | def my_stfc_connection(request_context=None, REQUTEXT=""): 27 | print("stfc connection invoked") 28 | print("request_context", request_context) 29 | print(f"REQUTEXT: {REQUTEXT}") 30 | 31 | # raise ExternalRuntimeError(**errorInfo) 32 | # raise ABAPRuntimeError(**errorInfo) 33 | # raise ABAPApplicationError(**errorInfo) 34 | 35 | return {"ECHOTEXT": REQUTEXT, "RESPTEXT": "Python server here"} 36 | 37 | 38 | # create server 39 | server = Server(*BACKEND[backend_dest]) 40 | 41 | # expose python function my_stfc_connection as ABAP function STFC_CONNECTION, 42 | # to be called by ABAP system 43 | server.add_function("STFC_CONNECTION", my_stfc_connection) 44 | 45 | # start server 46 | server.start() 47 | 48 | input("Press Enter to stop server...") 49 | 50 | # stop server 51 | server.stop() 52 | print("Server stoped") 53 | -------------------------------------------------------------------------------- /examples/clientStfcStructure.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import datetime 6 | from configparser import ConfigParser 7 | from os import path 8 | from pprint import pprint 9 | 10 | from pyrfc import Connection 11 | 12 | imp = { 13 | "RFCINT1": 0x7F, # INT1: Integer value (1 byte) 14 | "RFCINT2": 0x7FFE, # INT2: Integer value (2 bytes) 15 | "RFCINT4": 0x7FFFFFFE, # INT: integer value (4 bytes) 16 | "RFCFLOAT": 1.23456789, # FLOAT 17 | "RFCCHAR1": "a", # CHAR[1] 18 | "RFCCHAR2": "ij", # CHAR[2] 19 | "RFCCHAR4": "bcde", # CHAR[4] 20 | "RFCDATA1": "k" * 50, 21 | "RFCDATA2": "l" * 50, # CHAR[50] each 22 | "RFCTIME": datetime.time( 23 | 12, 24 | 34, 25 | 56, 26 | ), # TIME 27 | "RFCDATE": datetime.date( 28 | 2012, 29 | 10, 30 | 3, 31 | ), # DATE 32 | "RFCHEX3": b"\x66\x67\x68", # BYTE[3]: String with 3 hexadecimal values (='fgh') 33 | } 34 | 35 | 36 | def main(): 37 | config = ConfigParser() 38 | config.read( 39 | path.join( 40 | path.dirname(path.abspath(__file__)), 41 | "pyrfc.cfg", 42 | ) 43 | ) 44 | params_connection = dict(config.items("coevi51")) 45 | 46 | with Connection(**params_connection) as client: 47 | pprint( 48 | client.call( 49 | "STFC_STRUCTURE", 50 | IMPORTSTRUCT=imp, 51 | ) 52 | ) 53 | 54 | 55 | if __name__ == "__main__": 56 | main() 57 | -------------------------------------------------------------------------------- /tests/server/server.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import os 6 | 7 | from pyrfc import RCStatus, Server, set_ini_file_directory 8 | 9 | 10 | # server function 11 | def my_stfc_connection( 12 | request_context=None, 13 | REQUTEXT="", 14 | ): 15 | print("stfc invoked") 16 | print( 17 | "request_context", 18 | request_context, 19 | ) 20 | print(f"REQUTEXT: {REQUTEXT}") 21 | 22 | return { 23 | "ECHOTEXT": REQUTEXT, 24 | "RESPTEXT": "Python server here", 25 | } 26 | 27 | 28 | # server authorisation check 29 | def my_auth_check( 30 | func_name=False, 31 | request_context=None, 32 | ): 33 | print(f"authorization check for '{func_name}'") 34 | print( 35 | "request_context", 36 | request_context or {}, 37 | ) 38 | return RCStatus.OK 39 | 40 | 41 | dir_path = os.path.dirname(os.path.realpath(__file__)) 42 | set_ini_file_directory(dir_path) 43 | 44 | # create server instance 45 | server = Server( 46 | server_params={"dest": "gateway"}, 47 | client_params={"dest": "MME"}, 48 | config={ 49 | "port": 8081, 50 | "server_log": False, 51 | }, 52 | ) 53 | 54 | # expose python function my_stfc_connection as ABAP function STFC_CONNECTION, 55 | # that ABAP server can call 56 | server.add_function("STFC_CONNECTION", my_stfc_connection) 57 | 58 | try: 59 | server.start() 60 | 61 | input("Press Enter to stop server...\n") 62 | 63 | server.stop() 64 | except Exception as ex: 65 | print(ex) 66 | -------------------------------------------------------------------------------- /doc/tables/server_error.csv: -------------------------------------------------------------------------------- 1 | ===================== ================ ========================= ================= ============== =========== 2 | Error type Corresponds to In C triggered via Fields to fill in Effect on Effect in t 3 | ABAP statement return code error info connection 4 | ===================== ================ ========================= ================= ============== =========== 5 | ABAP exception RAISE RFC_ABAP_EXCEPTION key Remains open SY-SUBRC is 6 | 7 | ABAP exception with d MESSAGE ... RFC_ABAP_EXCEPTION key, Remains open As above. T 8 | RAISING abapMsgType 9 | abapMsgClass 10 | abapMsgNumber 11 | abapMsgV1-V4 12 | ABAP message MESSAGE ... RFC_ABAP_MESSAGE abapMsgType Is closed SY-SUBRC is 13 | abapMsgClass 14 | abapMsgNumber 15 | abapMsgV1-V4 16 | System failure RC_EXTERNAL_FAILURE message Is closed SY-SUBRC is 17 | ===================== ================ ========================= ================= ============== =========== 18 | -------------------------------------------------------------------------------- /tests/test_wsrfc.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import pytest 6 | from pyrfc import Connection, ExternalRuntimeError, set_cryptolib_path 7 | 8 | from tests.config import CryptoLibPath # ClientPSEPath, 9 | 10 | 11 | class TestWsrfc: 12 | @pytest.mark.skip(reason="no automatic test") 13 | def test_load_cryptolib_success(self): 14 | assert set_cryptolib_path(CryptoLibPath) is None 15 | 16 | def test_load_cryptolib_error(self): 17 | wrongPath = "_" + CryptoLibPath 18 | with pytest.raises(TypeError) as ex: 19 | assert set_cryptolib_path(wrongPath) 20 | error = ex.value 21 | assert error.args[0] == "Crypto library not found:" 22 | assert error.args[1] == wrongPath 23 | 24 | def test_wsrfc_call_no_client_pse(self): 25 | with pytest.raises(ExternalRuntimeError) as ex: 26 | Connection(dest="WS_ALX_NOCC") 27 | error = ex.value 28 | assert error.code == 20 29 | assert error.key == "RFC_INVALID_PARAMETER" 30 | assert error.message == "Unable to use TLS with client PSE missing" 31 | 32 | @pytest.mark.skip(reason="no automatic test") 33 | def test_wsrfc_call_basic_auth(self): 34 | client = Connection(dest="WS_ALX") 35 | assert client.alive is True 36 | client.close() 37 | 38 | @pytest.mark.skip(reason="no automatic test") 39 | def test_wsrfc_call_client_cert(self): 40 | client = Connection(dest="WS_ALX_CC") 41 | assert client.alive is True 42 | client.close() 43 | -------------------------------------------------------------------------------- /src/pyrfc/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | """pyrfc package.""" 6 | 7 | import os 8 | from contextlib import suppress 9 | from importlib.metadata import version 10 | 11 | __version__ = version(__name__) 12 | __version_info__ = tuple(__version__.split(".")) 13 | 14 | if os.name == "nt": 15 | # add SAP NWRFC SDK to DLL pth 16 | with suppress(Exception): 17 | os.add_dll_directory(os.path.join(os.environ["SAPNWRFC_HOME"], "lib")) 18 | 19 | from pyrfc._exception import ( 20 | ABAPApplicationError, 21 | ABAPRuntimeError, 22 | CommunicationError, 23 | ExternalApplicationError, 24 | ExternalAuthorizationError, 25 | ExternalRuntimeError, 26 | LogonError, 27 | RFCError, 28 | RFCLibError, 29 | ) 30 | 31 | try: 32 | from pyrfc._cyrfc import ( 33 | Connection, 34 | ConnectionParameters, 35 | Decimal, 36 | FunctionDescription, 37 | RCStatus, 38 | RfcFieldType, 39 | RfcParameterDirection, 40 | Server, 41 | Throughput, 42 | TypeDescription, 43 | UnitCallType, 44 | UnitState, 45 | cancel_connection, 46 | get_nwrfclib_version, 47 | language_iso_to_sap, 48 | language_sap_to_iso, 49 | reload_ini_file, 50 | set_cryptolib_path, 51 | set_ini_file_directory, 52 | set_locale_radix, 53 | ) 54 | except Exception as ex: 55 | # PyRFC module could not be loaded 56 | print(ex) 57 | 58 | __author__ = "SAP SE" 59 | __email__ = "srdjan.boskovic@sap.com" 60 | -------------------------------------------------------------------------------- /examples/server/server_app_thread.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | from threading import Thread 6 | 7 | from pyrfc import Server 8 | 9 | 10 | def my_stfc_connection(request_context=None, REQUTEXT=""): 11 | """Server function my_stfc_connection with the signature 12 | of ABAP function module STFC_CONNECTION.""" 13 | 14 | print("stfc connection invoked") 15 | print("request_context", request_context) 16 | print(f"REQUTEXT: {REQUTEXT}") 17 | 18 | return { 19 | "ECHOTEXT": REQUTEXT, 20 | "RESPTEXT": "Python server here", 21 | } 22 | 23 | 24 | def my_auth_check(func_name=False, request_context=None): 25 | """Server authorization check.""" 26 | 27 | if request_context is None: 28 | request_context = {} 29 | print(f"authorization check for '{func_name}'") 30 | print("request_context", request_context) 31 | return 0 32 | 33 | 34 | def launch_server(): 35 | """Start server.""" 36 | 37 | # create server for ABAP system ABC 38 | server = Server( 39 | server_params={"dest": "gateway"}, 40 | client_params={"dest": "MME"}, 41 | config={ 42 | "check_date": False, 43 | "check_time": False, 44 | "port": 8081, 45 | "server_log": False, 46 | }, 47 | ) 48 | print(server.get_server_attributes()) 49 | 50 | # expose python function my_stfc_connection as ABAP function STFC_CONNECTION, 51 | # to be called by ABAP system 52 | server.add_function("STFC_CONNECTION", my_stfc_connection) 53 | 54 | # start server 55 | server.serve() 56 | 57 | # get server attributes 58 | print(server.get_server_attributes()) 59 | 60 | 61 | server_thread = Thread(target=launch_server) 62 | server_thread.start() 63 | 64 | input("Press Enter to stop server...") 65 | 66 | # stop server 67 | server_thread.join() 68 | print("Server stoped") 69 | -------------------------------------------------------------------------------- /tests/test_options.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | from pyrfc import Connection 6 | 7 | from tests.config import CONNECTION_DEST as params 8 | 9 | 10 | class TestOptions: 11 | def setup_method(self): 12 | self.conn = Connection(**params) 13 | assert self.conn.alive 14 | 15 | def teardown_method(self): 16 | self.conn.close() 17 | assert not self.conn.alive 18 | 19 | def test_pass_when_not_requested(self): 20 | PLNTY = "A" 21 | PLNNR = "00100000" 22 | NOT_REQUESTED = [ 23 | "ET_COMPONENTS", 24 | "ET_HDR_HIERARCHY", 25 | "ET_MPACKAGES", 26 | "ET_OPERATIONS", 27 | "ET_OPR_HIERARCHY", 28 | "ET_PRTS", 29 | "ET_RELATIONS", 30 | ] 31 | res = self.conn.call( 32 | "EAM_TASKLIST_GET_DETAIL", 33 | {"not_requested": NOT_REQUESTED}, 34 | IV_PLNTY=PLNTY, 35 | IV_PLNNR=PLNNR, 36 | ) 37 | assert len(res["ET_RETURN"]) == 0 38 | 39 | def test_error_when_all_requested(self): 40 | PLNTY = "A" 41 | PLNNR = "00100000" 42 | res = self.conn.call( 43 | "EAM_TASKLIST_GET_DETAIL", 44 | IV_PLNTY=PLNTY, 45 | IV_PLNNR=PLNNR, 46 | ) 47 | assert len(res["ET_RETURN"]) == 1 48 | assert res["ET_RETURN"][0] == { 49 | "TYPE": "E", 50 | "ID": "DIWP1", 51 | "NUMBER": "212", 52 | "MESSAGE": "Task list A 00100000 is not hierarchical", 53 | "LOG_NO": "", 54 | "LOG_MSG_NO": "000000", 55 | "MESSAGE_V1": "A", 56 | "MESSAGE_V2": "00100000", 57 | "MESSAGE_V3": "", 58 | "MESSAGE_V4": "", 59 | "PARAMETER": "HIERARCHY", 60 | "ROW": 0, 61 | "FIELD": "", 62 | "SYSTEM": "MMECLNT620", 63 | } 64 | -------------------------------------------------------------------------------- /examples/server/server_pyrfc_thread.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | from pyrfc import Server 6 | 7 | 8 | def my_stfc_structure(request_context=None, IMPORTSTRUCT=None, RFCTABLE=None): 9 | """Server function my_stfc_structure with the signature 10 | of ABAP function module STFC_STRUCTURE.""" 11 | 12 | print("stfc structure invoked") 13 | print("request_context", request_context) 14 | if IMPORTSTRUCT is None: 15 | IMPORTSTRUCT = {} 16 | if RFCTABLE is None: 17 | RFCTABLE = [] 18 | ECHOSTRUCT = IMPORTSTRUCT.copy() 19 | ECHOSTRUCT["RFCINT1"] += 1 20 | ECHOSTRUCT["RFCINT2"] += 1 21 | ECHOSTRUCT["RFCINT4"] += 1 22 | if len(RFCTABLE) == 0: 23 | RFCTABLE = [ECHOSTRUCT] 24 | RESPTEXT = f"Python server sends {len(RFCTABLE)} table rows" 25 | print(f"ECHOSTRUCT: {ECHOSTRUCT}") 26 | print(f"RFCTABLE: {RFCTABLE}") 27 | print(f"RESPTEXT: {RESPTEXT}") 28 | 29 | return {"ECHOSTRUCT": ECHOSTRUCT, "RFCTABLE": RFCTABLE, "RESPTEXT": RESPTEXT} 30 | 31 | 32 | def my_auth_check(func_name=False, request_context=None): 33 | """Server authorization check.""" 34 | 35 | if request_context is None: 36 | request_context = {} 37 | print(f"authorization check for '{func_name}'") 38 | print("request_context", request_context) 39 | return 0 40 | 41 | 42 | # create server 43 | server = Server( 44 | server_params={"dest": "MME_GATEWAY"}, 45 | client_params={"dest": "MME"}, 46 | config={"check_date": False, "check_time": False, "server_log": True}, 47 | ) 48 | 49 | print(server.options) 50 | 51 | # expose python function my_stfc_structure as ABAP function STFC_STRUCTURE 52 | # to be called from ABAP system 53 | server.add_function("STFC_STRUCTURE", my_stfc_structure) 54 | 55 | # start server 56 | server.start() 57 | 58 | # get server attributes 59 | print(server.get_server_attributes()) 60 | 61 | input("Press Enter to stop server...") 62 | 63 | # shutdown server 64 | server.close() 65 | -------------------------------------------------------------------------------- /tests/test_package.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import sys 6 | from contextlib import suppress 7 | from os import listdir 8 | from os.path import isfile, join 9 | 10 | import pytest 11 | 12 | from tests.config import latest_python_version 13 | 14 | with suppress(ModuleNotFoundError): 15 | import tomllib 16 | 17 | 18 | @pytest.mark.skipif( 19 | "tomllib" not in sys.modules 20 | or "linux" not in sys.platform 21 | or sys.version_info < latest_python_version, 22 | reason="package check on latest python only", 23 | ) 24 | class TestPackageContent: 25 | """Wheel and sdist package content check.""" 26 | 27 | def setup_class(self): 28 | self.package_name = "pyrfc" 29 | with open("pyproject.toml", "rb") as file: 30 | pyproject = tomllib.load(file) 31 | self.package_name = pyproject["project"]["name"] 32 | self.version = pyproject["project"]["version"] 33 | self.temp_dir = join(".tox", "pack", "tmp") 34 | # assert subprocess.call(["bash", "tests/build_test.sh"]) == 0 35 | 36 | def test_wheel_package(self): 37 | package_path = join(self.temp_dir, self.package_name) 38 | package_files = [ 39 | fn for fn in listdir(package_path) if isfile(join(package_path, fn)) 40 | ] 41 | exts = set() 42 | # no cython and c sources, only python and 'so' 43 | for fn in package_files: 44 | exts.add(fn.rsplit(".")[-1]) 45 | assert exts == {"py", "so"} 46 | 47 | def test_sdist_package(self): 48 | sdist_path = join( 49 | self.temp_dir, 50 | f"{self.package_name}-{self.version}", 51 | "src", 52 | self.package_name, 53 | ) 54 | sdist_files = [fn for fn in listdir(sdist_path) if isfile(join(sdist_path, fn))] 55 | print(sdist_files) 56 | exts = set() 57 | # python, cython and c sources, no 'so' 58 | for fn in sdist_files: 59 | exts.add(fn.rsplit(".")[-1]) 60 | assert exts == {"py", "pxd", "pyx", "cpp"} 61 | -------------------------------------------------------------------------------- /examples/serverFunctionDescription.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | from pyrfc import FunctionDescription, TypeDescription 6 | 7 | animals = TypeDescription( 8 | "ANIMALS", 9 | nuc_length=20, 10 | uc_length=28, 11 | ) 12 | animals.add_field( 13 | name="LION", 14 | field_type="RFCTYPE_CHAR", 15 | nuc_length=5, 16 | uc_length=10, 17 | nuc_offset=0, 18 | uc_offset=0, 19 | ) 20 | animals.add_field( 21 | name="ELEPHANT", 22 | field_type="RFCTYPE_FLOAT", 23 | decimals=16, 24 | nuc_length=8, 25 | uc_length=8, 26 | nuc_offset=8, 27 | uc_offset=16, 28 | ) 29 | animals.add_field( 30 | name="ZEBRA", 31 | field_type="RFCTYPE_INT", 32 | nuc_length=4, 33 | uc_length=4, 34 | nuc_offset=16, 35 | uc_offset=24, 36 | ) 37 | 38 | func_desc = FunctionDescription("I_DONT_EXIST") 39 | func_desc.add_parameter( 40 | name="DOC", 41 | field_type="RFCTYPE_INT", 42 | direction="RFC_IMPORT", 43 | nuc_length=4, 44 | uc_length=4, 45 | ) 46 | func_desc.add_parameter( 47 | name="CAT", 48 | field_type="RFCTYPE_CHAR", 49 | direction="RFC_IMPORT", 50 | nuc_length=5, 51 | uc_length=10, 52 | ) 53 | func_desc.add_parameter( 54 | name="ZOO", 55 | field_type="RFCTYPE_STRUCTURE", 56 | direction="RFC_IMPORT", 57 | nuc_length=20, 58 | uc_length=28, 59 | type_description=animals, 60 | ) 61 | func_desc.add_parameter( 62 | name="BIRD", 63 | field_type="RFCTYPE_FLOAT", 64 | direction="RFC_IMPORT", 65 | nuc_length=8, 66 | uc_length=8, 67 | decimals=16, 68 | ) 69 | func_desc.add_parameter( 70 | name="COW", 71 | field_type="RFCTYPE_CHAR", 72 | direction="RFC_EXPORT", 73 | nuc_length=3, 74 | uc_length=6, 75 | ) 76 | func_desc.add_parameter( 77 | name="STABLE", 78 | field_type="RFCTYPE_STRUCTURE", 79 | direction="RFC_EXPORT", 80 | nuc_length=20, 81 | uc_length=28, 82 | type_description=animals, 83 | ) 84 | func_desc.add_parameter( 85 | name="HORSE", 86 | field_type="RFCTYPE_INT", 87 | direction="RFC_EXPORT", 88 | nuc_length=4, 89 | uc_length=4, 90 | ) 91 | -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: node-rfc 3 | Upstream-Contact: Srdjan Boskovic 4 | Source: https://github.com/SAP/node-rfc 5 | Disclaimer: The code in this project may include calls to APIs (“API Calls”) of 6 | SAP or third-party products or services developed outside of this project 7 | (“External Products”). 8 | “APIs” means application programming interfaces, as well as their respective 9 | specifications and implementing code that allows software to communicate with 10 | other software. 11 | API Calls to External Products are not licensed under the open source license 12 | that governs this project. The use of such API Calls and related External 13 | Products are subject to applicable additional agreements with the relevant 14 | provider of the External Products. In no event shall the open source license 15 | that governs this project grant any rights in or to any External Products,or 16 | alter, expand or supersede any terms of the applicable additional agreements. 17 | If you have a valid license agreement with SAP for the use of a particular SAP 18 | External Product, then you may make use of any API Calls included in this 19 | project’s code for that SAP External Product, subject to the terms of such 20 | license agreement. If you do not have a valid license agreement for the use of 21 | a particular SAP External Product, then you may only make use of any API Calls 22 | in this project for that SAP External Product for your internal, non-productive 23 | and non-commercial test and evaluation of such API Calls. Nothing herein grants 24 | you any rights to use or access any SAP External Product, or provide any third 25 | parties the right to use of access any SAP External Product, through API Calls. 26 | 27 | # Source code, documentation, tests, examples 28 | 29 | Files: setup.py tox.ini pyproject.toml src/** doc/** tests/** examples/** LICENCE LICENSES/** 30 | Copyright: 2015-2023 SAP SE Srdjan Boskovic 31 | License: Apache-2.0 32 | 33 | # Compiled sources, metadata, configuration 34 | 35 | Files: ci/* *.md *.json *.ini .gitignore .editorconfig .prettierignore CHANGES pytest.ini sapnwrfc.cfg MANIFEST.in 36 | Copyright: 2015-2023 SAP SE Srdjan Boskovic 37 | License: Apache-2.0 38 | -------------------------------------------------------------------------------- /tests/test_function_group_mrfc.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | # Note: Some test cases for the exception testing are taken 6 | # from the c-connector test cases 7 | # /bas/BIN/src/krn/rfc/nwrfc/testFramework/clienttests/ExceptionTest.c 8 | # Furthermore, the python script error_test.py in this directory 9 | # provides more test cases. Some of them are used as well. 10 | 11 | import unittest 12 | 13 | from pyrfc import Connection 14 | 15 | from tests.config import CONNECTION_PARAMS as params 16 | 17 | 18 | class TestMRFC: 19 | """ 20 | This test cases cover selected functions from the MRFC function group. 21 | """ 22 | 23 | def setup_method(self): 24 | self.conn = Connection(**params) 25 | assert self.conn.alive 26 | 27 | def test_info(self): 28 | connection_info = self.conn.get_connection_attributes() 29 | assert connection_info["isoLanguage"] == "EN" 30 | 31 | def teardown_method(self): 32 | self.conn.close() 33 | assert not self.conn.alive 34 | 35 | """ 36 | @unittest.skip("not remote-enabled") 37 | def test_ABAP4_CALL_TRANSACTION_VB(self): 38 | # ABAP4_CALL_TRANSACTION_VB 39 | pass 40 | 41 | @unittest.skip("not remote-enabled") 42 | def test_IS_VERIRUN_ACTIVE(self): 43 | # IS_VERIRUN_ACTIVE Determine Whether a Verification Run is Active 44 | pass 45 | 46 | @unittest.skip("not supported yet (trfc)") 47 | def test_RFC_CALL_TRANSACTION_USING(self): 48 | # RFC_CALL_TRANSACTION_USING 49 | # Verification Program for Execution of RFCs via CALL TRANSACTION USING 50 | pass 51 | 52 | # ToDo: Class based exceptions 53 | def test_RFC_CLASS_BASED_EXCP(self): 54 | # RFC_CLASS_BASED_EXCP RFC mit klassenbasierten Exceptions 55 | pass 56 | 57 | # TODO: How to test? 58 | @unittest.skip("not supported yet") 59 | def test_RFC_PING_AND_WAIT(self): 60 | # RFC_PING_AND_WAIT Aufruf und Warten 61 | pass 62 | """ 63 | 64 | """ 65 | @unittest.skip("not remote-enabled") 66 | def test_RFC_RAISE_ERROR_VB(self): 67 | # RFC_RAISE_ERROR_VB Behandlung von RFC-Methoden in Verbuchung 68 | pass 69 | 70 | @unittest.skip("not supported yet (xml)") 71 | def test_RFC_XML_TEST_1(self): 72 | # RFC_XML_TEST_1 Test xml stream 73 | pass 74 | """ 75 | 76 | 77 | if __name__ == "__main__": 78 | unittest.main() 79 | -------------------------------------------------------------------------------- /doc/remarks.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: pyrfc 2 | 3 | ======= 4 | Remarks 5 | ======= 6 | 7 | RFC call results differ from expected 8 | ===================================== 9 | 10 | The response when invoking ABAP function module directly via transaction SE37 11 | may be different from the response when called via Python connector. Typically leading 12 | zeros are not shown in SE37 but sent to Python when remote FM is called from Python. 13 | 14 | This is surely a bug and in general there are few possible causes: 15 | the ``pyrfc`` package, the *SAP NW RFC Library*, or the function 16 | module itself. Bugs in *SAP NW RFC Library* are unlikely, in ``pyrfc`` less likely and 17 | the cause is usually the implementation of the ABAP FM, not respecting technical restriction 18 | described `here `_. 19 | 20 | Conversion ("ALPHA") exists are triggered in SAP GUI and in SE37 but not when 21 | the ABAP FM is called via SAP RFC protocol, therefore the rectifying logic shall be 22 | implemented in the FM. 23 | The behaviour can be also caised by different authorization levels, when ABAP FM is invoked 24 | locally or remotely, via SAP RFC protocol. 25 | 26 | Please try to assure that RFC is working (e.g. testing with the C connector), 27 | before reporting the problem. 28 | 29 | 30 | Multiple threads 31 | ================ 32 | 33 | If you work with a multiple thread environment, assure that each thread has 34 | its own ``Connection`` object. The reason is that during the RFC calls, 35 | the Python global interpreter lock (GIL) is released. 36 | 37 | 38 | Open, valid, and alive connections 39 | ================================== 40 | 41 | A connection may be closed by the client (e.g. via :meth:`Connection.close`) or 42 | by the server (e.g. if an ABAP message is raised). The connection object 43 | maintains a object variable ``alive`` to record the actual state. 44 | However, the variable is of internal use only, as the connection object 45 | *will try to reopen the connection, if needed*. 46 | This leads to the -- seemingly strange -- situation that an explicitly closed 47 | connection will allow you to call, e.g. :meth:`Connection.ping`. 48 | 49 | Implementation remark: It is not possible to query the actual state of the 50 | connection in a reliable manner. Although *SAP NW RFC Library* offers the function 51 | ``RfcIsConnectionHandleValid()``, it will only return *False* if the connection 52 | was closed by the client, not if it was closed from the server side. Therefore, 53 | an explicit object variable is kept. 54 | -------------------------------------------------------------------------------- /examples/server/README.md: -------------------------------------------------------------------------------- 1 | # Background RFC Server 2 | 3 | ## Configuration 4 | 5 | ### ALX 6 | 7 | Configure RFC destination using SM59 transaction: 8 | 9 | - TCP/IP Connection `NWRFC_SERVER_OS` 10 | - Special Options > Select Protocol: basXML serializer 11 | 12 | ![Protocol basXML Serializer](doc/sm59-serializer.png) 13 | 14 | ### MME 15 | 16 | Configure bgRFC queues using transaction `SBGRFCCONF` 17 | 18 | Scheduler app server and destination 19 | 20 | ![Scheduler: App Server](doc/SBGRFCCONF-Scheduler-App-Server.png) 21 | 22 | ![Scheduler: Sestination](doc/SBGRFCCONF-Scheduler-Destination.png) 23 | 24 | Configure inbound destination prefixes for bgRFC queues' names. Other queue names are processed as standard RFC queues. 25 | 26 | ![Inbound Destination Prefixes](doc/SBGRFCCONF-Define-Inbound-Destination.png) 27 | 28 | ### Python 29 | 30 | - `sapnwrfc.ini` 31 | 32 | ```ini 33 | DEST=ALX 34 | MSHOST=ldcialx 35 | MSSERV=9914 36 | GROUP=PUBLIC 37 | CLIENT=000 38 | LANG=EN 39 | USER=BOSKOVIC 40 | PASSWD=Kol!ko11 41 | 42 | DEST=ALX_GATEWAY 43 | GWSERV=sapgw18 44 | GWHOST=ldai5alx.wdf.sap.corp 45 | PROGRAM_ID=RFCSERVER 46 | REG_COUNT=1 47 | 48 | DEST=BCO 49 | MSHOST=ldcsbco 50 | MSSERV=5357 51 | GROUP=PUBLIC 52 | CLIENT=000 53 | LANG=EN 54 | USER=NWRFCTEST 55 | PASSWD=Welcome1 56 | ``` 57 | 58 | ## Test 59 | 60 | ### Python 61 | 62 | ```python 63 | python examples/server/bgrfc_server.py ALX | MME | QM7 64 | ``` 65 | 66 | ### ALX 67 | 68 | 1. Start Python server 69 | 70 | 2. Run test report `z_nwrfc_server_bgrfc` with parameters: 71 | 72 | - QNAME_O: BASIS_BGRFC_OUTIN 73 | - QNAME_IN: NCO_CONN_JUNIT_QUEUE_BC 74 | - RFCDEST: NWRFC_SERVER_OS 75 | - LOCK_OUT: X 76 | - LOCK_IN: X 77 | 78 | 3. Run transaction `SBGRFCMON`, select outbound queues for destination NWRFC_SERVER_OS 79 | 80 | 4. Delete queue lock to submit the queue to Python 81 | 82 | 83 | 84 | ### MME 85 | 86 | 1. Start Python server 87 | 88 | 2. Run test report `z_nwrfc_server_bgrfc` with parameters: 89 | 90 | - QNAME_O: BASIS_BGRFC_OUTIN 91 | - QNAME_IN: RFCSDK_QUEUE_IN 92 | - RFCDEST: NWRFC_SERVER_OS 93 | - LOCK_OUT: X 94 | - LOCK_IN: X 95 | 96 | 3. Run transaction `SBGRFCMON` and find the locked queue `RFCSDK_QUEUE_IN` 97 | 98 | 4. Delete the lock, so that queue gets executed. 99 | 100 | Run SE16 and check TCPIC table 101 | 102 | ```ini 103 | DEST=gateway 104 | GWSERV=sapgw00 105 | GWHOST=coevi51 106 | PROGRAM_ID=SERVER1 107 | REG_COUNT=1 108 | 109 | DEST=MME 110 | USER=demo 111 | PASSWD=welcome 112 | ASHOST=coevi51.wdf.sap.corp 113 | #ASHOST=10.68.110.51 114 | SYSNR=00 115 | CLIENT=620 116 | LANG=EN 117 | #TRACE=3 118 | ``` 119 | -------------------------------------------------------------------------------- /doc/build.rst: -------------------------------------------------------------------------------- 1 | .. _build: 2 | 3 | ==================== 4 | Building from source 5 | ==================== 6 | 7 | After SAP NW RFC Library is installed as described in the :ref:`Installation 8 | `, the :mod:`pyrfc` repository shall be cloned so that you can build 9 | the distribution release wheel, source distribution and documentation. 10 | 11 | and Instead of downloading and installing precompiled egg, you 12 | need to prepare the toolchain and clone 13 | 14 | Toolchain preparation 15 | ===================== 16 | 17 | * Download and install SAP NWRFC SDK requirements, following `SAP Note 2573790 - Installation, Support and Availability of the SAP NetWeaver RFC Library 7.50 `_ 18 | 19 | * On Windows platform the `Visual C++ Redistributable Package for Visual Studio 2013 `_ is required at runtime, see `README # Windows Requirements `_ 20 | 21 | * On macOS platform Xcode command line tools are required, eventually also C++ development headers: 22 | 23 | .. code-block:: sh 24 | 25 | xcode-select --install 26 | sudo installer -pkg macOS_SDK_headers_for_macOS_10.15.pkg -target / 27 | 28 | Install pyrfc build tools 29 | 30 | .. code-block:: sh 31 | 32 | pip install build cython wheel pytest sphinx 33 | 34 | * To get any software from the Git source control system the Git 35 | client is required as well, use whatever your distribution has 36 | * The system must contain a ``gcc`` compiler as well as development 37 | header and library files as provided by your distribution 38 | 39 | Linting and Formatting 40 | ---------------------- 41 | 42 | Done by `tox` job 'lint_format' 43 | 44 | .. code-block:: ini 45 | 46 | cython-lint src/pyrfc --max-line-length=180 47 | ruff check --fix src setup.py tests examples --line-length=120 --ignore=F401 48 | pydocstyle src 49 | 50 | 51 | Build from source 52 | ----------------- 53 | 54 | .. code-block:: sh 55 | 56 | cd PyRFC 57 | python -m build --wheel --sdist --no-isolation --outdir dist 58 | 59 | # or 60 | 61 | python -m pip install . 62 | 63 | 64 | Building the documentation 65 | ========================== 66 | 67 | Ensure that the lib directory of the SAP NW RFC library is in your PATH environment. 68 | 69 | Run the `tox` job for building the documentation and copy the output to `gh-pages` branch 70 | 71 | .. code-block:: sh 72 | 73 | tox -e docs 74 | git checkout gh-pages 75 | yes | cp -R .tox/docs/tmp/html/. . 76 | 77 | 78 | Commit and push the changes to `gh-pages` branch. 79 | 80 | .. note:: 81 | 82 | An additional file .nojekyll is placed in ``gh-pages`` to disable the default GitHub processing which breaks sphinx style folders with leading underscores. 83 | -------------------------------------------------------------------------------- /examples/server/trfc_test.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | from pyrfc import Connection, RCStatus, Server 4 | 5 | 6 | def stfc_write_to_tcpic(request_context=None, RESTART_QNAME="", TCPICDAT=[]): 7 | context = ( 8 | {"error": "No request context"} 9 | if request_context is None 10 | else request_context["server_context"] 11 | ) 12 | print("[js] python function: stfc_write_to_tcpic", f"context {context}") 13 | return {"TCPICDAT": TCPICDAT} 14 | 15 | 16 | def onCheckTransaction(rfcHandle, tid): 17 | print("[js] onCheckTransaction", rfcHandle, tid) 18 | return RCStatus.OK 19 | 20 | 21 | def onCommitTransaction(rfcHandle, tid): 22 | print("[js] onCommitTransaction", rfcHandle, tid) 23 | return RCStatus.OK 24 | 25 | 26 | def onConfirmTransaction(rfcHandle, tid): 27 | print("[js] onConfirmTransaction", rfcHandle, tid) 28 | return RCStatus.OK 29 | 30 | 31 | def onRollbackTransaction(rfcHandle, tid): 32 | print("[js] onRollbackTransaction", rfcHandle, tid) 33 | return RCStatus.OK 34 | 35 | 36 | def authenticationHandler(function_name, server_context=None): 37 | print(f"[js] authentication for {function_name}, context {server_context}") 38 | return RCStatus.OK 39 | 40 | 41 | def authorizationHandler(rfcHandle=None, securityAttributes=None): 42 | print(f"[js] authorization for {rfcHandle}, security attr {securityAttributes}") 43 | return RCStatus.OK 44 | 45 | 46 | # Create server 47 | server = Server( 48 | server_params={"dest": "MME_GATEWAY"}, 49 | client_params={"dest": "MME"}, 50 | config={ 51 | "authentication_check": authenticationHandler, 52 | "authorization_check": authorizationHandler, 53 | "check_date": False, 54 | "check_time": False, 55 | "server_log": True, 56 | }, 57 | ) 58 | 59 | # ABAP function used to send IDocs via tRFC/qRFC 60 | server.add_function("STFC_WRITE_TO_TCPIC", stfc_write_to_tcpic) 61 | 62 | # Register the RFC transaction handlers. 63 | server.transaction_rfc_init( 64 | sysId="MME", 65 | transactionHandler={ 66 | "check": onCheckTransaction, 67 | "commit": onCommitTransaction, 68 | "rollback": onRollbackTransaction, 69 | "confirm": onConfirmTransaction, 70 | }, 71 | ) 72 | 73 | try: 74 | # Start server 75 | server.start() 76 | 77 | # Get server attributes 78 | print(server.get_server_attributes()) 79 | 80 | # Check transaction handlers 81 | # print(server.transaction_handlers) 82 | # print(server.transaction_handlers_count) 83 | 84 | # call RSARFCT0 85 | client = Connection(dest="MME") 86 | ncall = client.call("ZSERVER_TEST_TRFC", NCALL="00001")["EV_NCALL"] 87 | print("queues sent", ncall) 88 | client.close() 89 | 90 | # receive queues from abap system 91 | sleep(5) 92 | 93 | except Exception as ex: 94 | print(ex) 95 | 96 | finally: 97 | # Shutdown server 98 | server.close() 99 | -------------------------------------------------------------------------------- /tests/test_timeout.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import pytest 6 | from pyrfc import Connection, RFCError, ExternalRuntimeError 7 | 8 | from tests.config import CONNECTION_DEST 9 | 10 | client = Connection(**CONNECTION_DEST) 11 | 12 | 13 | class TestTimeout: 14 | def test_timeout_call(self): 15 | with pytest.raises(RFCError) as ex: 16 | # Cancel 10 seconds long RFC call after 5 seconds 17 | client.call( 18 | "RFC_PING_AND_WAIT", 19 | options={"timeout": 5}, 20 | SECONDS=10, 21 | ) 22 | error = ex.value 23 | assert error.code == 7 24 | assert error.key == "RFC_CANCELED" 25 | assert "Connection was canceled" in error.message 26 | # ensure new connection replaced the canceled one 27 | assert client.alive is True 28 | 29 | def test_timeout_call_expired(self): 30 | old_handle = client.handle 31 | # Cancel 5 seconds long RFC call after 10 seconds 32 | res = client.call("RFC_PING_AND_WAIT", options={"timeout": 10}, SECONDS=5) 33 | assert res == {} 34 | assert client.alive is True 35 | assert client.handle == old_handle 36 | 37 | def test_timeout_connection(self): 38 | client = Connection(**CONNECTION_DEST, config={"timeout": 5}) 39 | with pytest.raises(RFCError) as ex: 40 | # Cancel 10 seconds long RFC call after 5 seconds, set on connection 41 | client.call("RFC_PING_AND_WAIT", SECONDS=10) 42 | error = ex.value 43 | assert error.code == 7 44 | assert error.key == "RFC_CANCELED" 45 | assert "Connection was canceled" in error.message 46 | # ensure new connection replaced the cancelled one 47 | assert client.alive is True 48 | 49 | def test_timeout_connection_override(self): 50 | client = Connection(**CONNECTION_DEST, config={"timeout": 15}) 51 | with pytest.raises(RFCError) as ex: 52 | # Cancel 10 seconds long RFC call after 5 seconds, set on call 53 | client.call( 54 | "RFC_PING_AND_WAIT", 55 | options={"timeout": 5}, 56 | SECONDS=10, 57 | ) 58 | error = ex.value 59 | assert error.code == 7 60 | assert error.key == "RFC_CANCELED" 61 | assert "Connection was canceled" in error.args[0] 62 | # ensure new connection replaced the canceled one 63 | assert client.alive is True 64 | 65 | def test_timeout_with_rfc_error(self): 66 | with pytest.raises(ExternalRuntimeError) as ex: 67 | client.call("STFC_CONNECTION", options={"timeout": 60}, undefined=0) 68 | error = ex.value 69 | assert error.code == 20 70 | assert error.key == "RFC_INVALID_PARAMETER" 71 | assert error.message == "field 'undefined' not found" 72 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["Cython >= 3.0.0", "setuptools >= 67", "wheel >= 0.40"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pyrfc" 7 | version = "3.4" 8 | readme = "README.md" 9 | license = { file = "LICENSES/Apache-2.0.txt" } 10 | description = "Python bindings for SAP NetWeaver RFC SDK" 11 | authors = [ { name = "SAP SE"}, { email = "srdjan.boskovic@sap.com" } ] 12 | maintainers = [ { name = "Srdjan Boskovic", email = "srdjan.boskovic@sap.com" } ] 13 | requires-python = ">=3.8" 14 | keywords = ["pyrfc", "sap", "nwrfc", "sapnwrfc", "abap"] 15 | classifiers = [ 16 | "Topic :: Software Development :: Build Tools", 17 | "Topic :: Software Development :: Libraries :: Python Modules", 18 | "Development Status :: 5 - Production/Stable", 19 | "Intended Audience :: Developers", 20 | "Natural Language :: English", 21 | "License :: OSI Approved :: Apache Software License", 22 | "Operating System :: OS Independent", 23 | "Programming Language :: Cython", 24 | "Programming Language :: Python", 25 | "Programming Language :: Python :: 3.8", 26 | "Programming Language :: Python :: 3.9", 27 | "Programming Language :: Python :: 3.10", 28 | "Programming Language :: Python :: 3.11", 29 | "Programming Language :: Python :: 3.12", 30 | "Programming Language :: Python :: 3 :: Only" 31 | ] 32 | dependencies = [] 33 | 34 | [project.urls] 35 | bug-tracker = "https://github.com/wntrblm/nox/issues" 36 | documentation = "http://sap.github.io/PyRFC" 37 | homepage = "https://github.com/SAP/PyRFC" 38 | repository = "https://github.com/SAP/PyRFC.git" 39 | 40 | [tool.cython-lint] 41 | max-line-length = 172 42 | 43 | [tool.setuptools] 44 | package-dir = {"" = "src"} 45 | include-package-data = false 46 | license-files=["LICENSES/*.txt"] 47 | 48 | [tool.setuptools.packages.find] 49 | where = ["src"] 50 | include = ["pyrfc"] 51 | 52 | [tool.pytest.ini_options] 53 | minversion = "7.4" 54 | testpaths =["tests"] 55 | 56 | [tool.ruff] 57 | preview = true 58 | respect-gitignore = true 59 | line-length = 88 60 | target-version = "py38" 61 | 62 | [tool.ruff.lint] 63 | select = [ 64 | "C90", # McCabe cyclomatic complexity 65 | "E", # pycodestyle 66 | "F", # Pyflakes 67 | "ICN", # flake8-import-conventions 68 | "INT", # flake8-gettext 69 | "PLC", # Pylint conventions 70 | "PLE", # Pylint errors 71 | "PLR09", # Pylint refactoring: max-args, max-branches, max returns, max-statements 72 | "PYI", # flake8-pyi 73 | "RSE", # flake8-raise 74 | "RUF", # Ruff-specific rules 75 | "T10", # flake8-debugger 76 | "TCH", # flake8-type-checking 77 | "TID", # flake8-tidy-imports 78 | "W", # pycodestyle 79 | "YTT", # flake8-2020 80 | ] 81 | 82 | [tool.ruff.lint.mccabe] 83 | max-complexity = 12 84 | 85 | [tool.ruff.lint.per-file-ignores] 86 | "src/pyrfc/__init__.py" = ["F401"] 87 | 88 | [tool.ruff.format] 89 | # same as Black 90 | quote-style = "double" 91 | indent-style = "space" 92 | skip-magic-trailing-comma = false 93 | line-ending = "auto" 94 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | description = "PyRFC Build" 3 | minversion = 4.5 4 | envlist = 5 | linter, docs, reuse, sdist, pack, 6 | py38, py39, py310, py311, py312 7 | package = wheel 8 | skip_missing_interpreters = false 9 | isolated_build = true 10 | 11 | [testenv:.pkg] 12 | passenv = SAPNWRFC_HOME, PYRFC_BUILD_CYTHON 13 | 14 | [testenv] 15 | description = Build binary wheels and run unit tests 16 | passenv = SAPNWRFC_HOME, PYRFC_BUILD_CYTHON 17 | deps = 18 | build 19 | Cython 20 | pytest 21 | pytest-testdox 22 | pytest-html-reporter 23 | commands = 24 | # build binary wheel for release 25 | python -m build --wheel --outdir dist 26 | # unit test 27 | python -m pytest tests --testdox --html-report=./report {posargs} 28 | 29 | [testenv:linter] 30 | description = Check linting 31 | platform = darwin 32 | basepython = python3.11 33 | skip_install = true 34 | deps = 35 | ruff 36 | cython-lint 37 | pydocstyle 38 | commands = 39 | cython-lint src/pyrfc --max-line-length=180 40 | ruff format src setup.py tests examples 41 | pydocstyle src 42 | 43 | [testenv:docs] 44 | description = Build sphinx documentation 45 | platform = darwin 46 | basepython = python3.11 47 | passenv = SAPNWRFC_HOME 48 | change_dir = doc 49 | allowlist_externals = 50 | sphinx-build 51 | deps = sphinx 52 | commands = 53 | sphinx-build -b html -a -E -d {envtmpdir}/doctrees . {envtmpdir}/html 54 | 55 | [testenv:gh-pages] 56 | description = Build gh-pages 57 | platform = darwin 58 | basepython = python3.11 59 | skip_install = true 60 | allowlist_externals = 61 | git 62 | rm 63 | cp 64 | commands = 65 | git checkout gh-pages 66 | rm -rf *.html searchindex.js objects.inv _images _sources _static 67 | cp -R {toxworkdir}/docs/tmp/html/. . 68 | git add . 69 | git commit -m "gh-pages update by tox" 70 | git checkout main 71 | 72 | [testenv:reuse] 73 | description = Reuse compliance check 74 | platform = darwin 75 | basepython = python3.11 76 | skip_install = true 77 | deps = reuse 78 | commands= 79 | python -m reuse lint 80 | 81 | [testenv:pypi-publish] 82 | description = Publish on PyPI 83 | platform = darwin 84 | basepython = python3.11 85 | skip_install = true 86 | deps = twine 87 | commands = 88 | python -m twine upload dist/*-macosx*.whl dist/*-win*.whl dist/*.tar.gz --verbose 89 | 90 | [testenv:pypi-test-publish] 91 | description = Publish on Test PyPI 92 | platform = darwin 93 | basepython = python3.11 94 | skip_install = true 95 | deps = twine 96 | commands = 97 | python -m twine upload --repository testpypi dist/*-macosx*.whl dist/*-win*.whl dist/*.tar.gz --verbose 98 | 99 | [testenv:pack] 100 | description = Build the test wheel for unit test 101 | platform = linux 102 | basepython = python3.11 103 | skip_install = true 104 | passenv = SAPNWRFC_HOME, PYRFC_BUILD_CYTHON 105 | allowlist_externals = 106 | find 107 | tar 108 | unzip 109 | deps = 110 | build 111 | setuptools 112 | Cython 113 | commands = 114 | python -m build --sdist --wheel --outdir {env_tmp_dir} 115 | find {env_tmp_dir} -name *.tar.gz -exec tar -xf {} -C {env_tmp_dir} \; 116 | unzip {env_tmp_dir}/\*.whl -d {env_tmp_dir} 117 | find {env_tmp_dir} '(' -name *.tar.gz -o -name *.whl ')' -exec rm {} \; 118 | 119 | [testenv:sdist] 120 | description = Build sdist 121 | platform = linux 122 | basepython = python3.11 123 | skip_install = true 124 | passenv = SAPNWRFC_HOME, PYRFC_BUILD_CYTHON 125 | deps = 126 | Cython 127 | build 128 | commands = 129 | python -m build --sdist --outdir dist 130 | -------------------------------------------------------------------------------- /tests/function_description_utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import sys 6 | 7 | from data.func_desc_STFC_STRUCTURE import FUNC_DESC_STFC_STRUCTURE 8 | from pyrfc import Connection 9 | 10 | parameter_keys = [ 11 | "name", 12 | "parameter_type", 13 | "direction", 14 | "nuc_length", 15 | "uc_length", 16 | "decimals", 17 | "default_value", 18 | "optional", 19 | "type_description", 20 | "parameter_text", 21 | ] 22 | 23 | typedesc_keys = [ 24 | "name", 25 | "field_type", 26 | "nuc_length", 27 | "nuc_offset", 28 | "uc_length", 29 | "uc_offset", 30 | "decimals", 31 | "type_description", 32 | ] 33 | 34 | 35 | def compare_function_description(fd1, fd2): 36 | # compare names 37 | fd1_name = fd1["name"] if isinstance(fd1, dict) else fd1.name 38 | fd2_name = fd2["name"] if isinstance(fd2, dict) else fd2.name 39 | # print(fd1_name, len(fd1_name)) 40 | # print(fd2_name, len(fd2_name)) 41 | assert fd1_name == fd2_name 42 | # compare parameters 43 | fd1_parameters = fd1["parameters"] if isinstance(fd1, dict) else fd1.parameters 44 | fd2_parameters = fd2["parameters"] if isinstance(fd2, dict) else fd2.parameters 45 | # print(len(fd1_parameters)) 46 | # print(len(fd2_parameters)) 47 | assert len(fd1_parameters) == len(fd2_parameters) 48 | for param1, param2 in zip(fd1_parameters, fd2_parameters): 49 | for key in parameter_keys: 50 | if key != "type_description": 51 | assert param1[key] == param2[key] 52 | if param1["type_description"] is None: 53 | assert param2["type_description"] is None 54 | else: 55 | # compare fields 56 | fd1_fields = ( 57 | param1["type_description"]["fields"] 58 | if isinstance(param1["type_description"], dict) 59 | else param1["type_description"].fields 60 | ) 61 | fd2_fields = ( 62 | param2["type_description"]["fields"] 63 | if isinstance(param2["type_description"], dict) 64 | else param2["type_description"].fields 65 | ) 66 | assert len(fd1_fields) == len(fd2_fields) 67 | assert fd1_fields == fd2_fields 68 | 69 | 70 | def function_description_to_dict(func_desc): 71 | FDD = { 72 | "name": func_desc.name, 73 | "parameters": [], 74 | } 75 | for parameter in func_desc.parameters: 76 | # print("parameter", parameter, parameter["type_description"]) 77 | param = {} 78 | for key in parameter_keys: 79 | if key == "type_description" and parameter[key] is not None: 80 | param[key] = { 81 | "name": parameter[key].name, 82 | "fields": parameter[key].fields, 83 | } 84 | else: 85 | param[key] = parameter[key] 86 | FDD["parameters"].append(param) 87 | return FDD 88 | 89 | 90 | # function description test data 91 | def main(func_name): 92 | with Connection(dest="MME") as client: 93 | func_desc = client.get_function_description(func_name) 94 | fd = function_description_to_dict(func_desc) 95 | # print(fd) # > fd.py 96 | compare_function_description( 97 | fd, 98 | FUNC_DESC_STFC_STRUCTURE, 99 | ) 100 | print("ok") 101 | 102 | 103 | if __name__ == "__main__": 104 | if len(sys.argv) < 2: 105 | print("No function name provided.") 106 | sys.exit() 107 | main(sys.argv[1]) 108 | -------------------------------------------------------------------------------- /doc/_static/images/rfm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 38 | 40 | 41 | 43 | image/svg+xml 44 | 46 | 47 | 48 | 49 | 50 | 54 | 61 | Function Modules (FM) 72 | 75 | 82 | Remote enabledFunction Modules (RFM) 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /examples/clientPrintDescription.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import sys 6 | from configparser import ConfigParser 7 | from os import path 8 | 9 | from pyrfc import ( 10 | ABAPApplicationError, 11 | ABAPRuntimeError, 12 | CommunicationError, 13 | Connection, 14 | LogonError, 15 | ) 16 | 17 | 18 | def parameter_key_function(parameter): 19 | """returns a key for sorting parameters""" 20 | direction = {"RFC_IMPORT": 1, "RFC_CHANGING": 2, "RFC_TABLES": 3, "RFC_EXPORT": 4} 21 | return direction[parameter["direction"]] 22 | 23 | 24 | def main(function_name): # noqa: PLR0912 25 | cp = ConfigParser() 26 | cp.read(path.join(path.dirname(path.abspath(__file__)), "pyrfc.cfg")) 27 | 28 | params_connection = dict(cp.items("coevi51")) 29 | print(params_connection) 30 | 31 | try: 32 | connection = Connection(**params_connection) 33 | func_desc = connection.get_function_description(function_name) 34 | print(f"Parameters of function: {function_name}") 35 | 36 | parameter_keys = [ 37 | "name", 38 | "parameter_type", 39 | "direction", 40 | "nuc_length", 41 | "uc_length", 42 | "decimals", 43 | "default_value", 44 | "optional", 45 | "type_description", 46 | "parameter_text", 47 | ] 48 | parameter_widths = [20, 17, 11, 10, 9, 9, 15, 10, 15, 20] 49 | for key, width in zip(parameter_keys, parameter_widths): 50 | sys.stdout.write(f"{key}".ljust(width).upper() + " ") 51 | sys.stdout.write("\n") 52 | 53 | for parameter in sorted(func_desc.parameters, key=parameter_key_function): 54 | for key, width in zip(parameter_keys, parameter_widths): 55 | if key == "type_description" and parameter[key] is not None: 56 | sys.stdout.write(f"{parameter[key].name}".ljust(width) + " ") 57 | else: 58 | sys.stdout.write(f"{parameter[key]}".ljust(width) + " ") 59 | sys.stdout.write("\n") 60 | type_desc = parameter["type_description"] 61 | if type_desc is not None: 62 | # type_desc of class TypeDescription 63 | field_keys = [ 64 | "name", 65 | "field_type", 66 | "nuc_length", 67 | "nuc_offset", 68 | "uc_length", 69 | "uc_offset", 70 | "decimals", 71 | "type_description", 72 | ] 73 | field_widths = [20, 17, 10, 10, 9, 9, 10, 15] 74 | 75 | sys.stdout.write( 76 | " -----------( Structure of {0.name} (n/uc_length={0.nuc_length}/{0.uc_length})--\n".format( # noqa: E501 77 | type_desc 78 | ) 79 | ) 80 | for key, width in zip(field_keys, field_widths): 81 | sys.stdout.write(f"{key}".ljust(width).upper() + " ") 82 | sys.stdout.write("\n") 83 | 84 | for field_description in type_desc.fields: 85 | for key, width in zip(field_keys, field_widths): 86 | sys.stdout.write(f"{field_description[key]}".ljust(width) + " ") 87 | sys.stdout.write("\n") 88 | sys.stdout.write( 89 | f" -----------( Structure of {type_desc.name} )-----------\n" 90 | ) 91 | sys.stdout.write("-" * sum(parameter_widths) + "\n") 92 | connection.close() 93 | 94 | except CommunicationError: 95 | print("Could not connect to server.") 96 | raise 97 | except LogonError: 98 | print("Could not log in. Wrong credentials?") 99 | raise 100 | except (ABAPApplicationError, ABAPRuntimeError): 101 | print("An error occurred.") 102 | raise 103 | 104 | 105 | if __name__ == "__main__": 106 | if len(sys.argv) < 2: 107 | print("No function name provided.") 108 | sys.exit() 109 | main(sys.argv[1]) 110 | -------------------------------------------------------------------------------- /examples/server/tlog.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import os 6 | import uuid 7 | from contextlib import suppress 8 | from datetime import datetime 9 | 10 | from pyrfc import UnitState 11 | 12 | # default dbpath 13 | DBPATH = "./examples/server/tlog.log" 14 | 15 | 16 | class TLog: 17 | """Server transaction log implementation example.""" 18 | 19 | def __len__(self): 20 | try: 21 | with open(self.__fn, "r") as fp: 22 | return len(fp.readlines()) 23 | except IOError: 24 | return 0 25 | 26 | def __getitem__(self, tid): 27 | itm = None 28 | try: 29 | with open(self.__fn, "r") as fp: 30 | for line in fp: 31 | if tid in line: 32 | itm = self.parse_line(line) 33 | return itm 34 | except IOError: 35 | return None 36 | 37 | def remove(self, tid=None): 38 | with suppress(IOError): 39 | if tid is None: 40 | os.remove(self.__fn) 41 | else: 42 | with open(self.__fn, "r") as fp: 43 | lines = fp.readlines() 44 | with open(self.__fn, "w") as fp: 45 | for line in lines: 46 | if tid not in line[:-1]: 47 | fp.write(line) 48 | 49 | def __contains__(self, tid): 50 | try: 51 | with open(self.__fn, "r") as fp: 52 | for line in fp: 53 | if tid in line: 54 | return True 55 | return False 56 | except IOError: 57 | return False 58 | 59 | def parse_line(self, line): 60 | try: 61 | ( 62 | utc_date, 63 | utc_time, 64 | tid, 65 | status, 66 | *note, 67 | ) = line.split() 68 | tid = { 69 | "utc": f"{utc_date} {utc_time}", 70 | "tid": tid, 71 | "status": status, 72 | } 73 | if len(note) > 0: 74 | tid["note"] = note[0] 75 | return tid 76 | except Exception: 77 | raise Exception(f"TLOG line format invalid: '{line}'") 78 | 79 | def getTIDs(self, tids=None): 80 | if isinstance(tids, str): 81 | tids = [tids] 82 | elif tids is None: 83 | tids = [] 84 | if len(tids) == 0: 85 | return [] 86 | else: 87 | TIDs = [] 88 | try: 89 | with open(self.__fn, "r") as fp: 90 | for line in fp: 91 | for td in tids: 92 | if td in line: 93 | TIDs.append(self.parse_line(line)) 94 | return TIDs 95 | except IOError: 96 | return [] 97 | 98 | def __init__(self, dbpath=DBPATH): 99 | self.__fn = dbpath 100 | 101 | def write(self, tid, status, note=None): 102 | if len(tid) != 32: 103 | raise Exception(f"TID length not 32: '{tid}'") 104 | if status not in UnitState: 105 | raise Exception(f"TID status '{status}' not supported for tid '{tid}'") 106 | note = " " + note if note is not None else "" 107 | tlog_record = f"{datetime.utcnow()} {tid} {status.name}{note}" 108 | with open(self.__fn, "a") as file: 109 | file.write(tlog_record + "\n") 110 | return self.parse_line(tlog_record) 111 | 112 | def uuid(self): 113 | return uuid.uuid4().hex.upper() 114 | 115 | def dump(self): 116 | with suppress(IOError), open(self.__fn, "r") as fp: 117 | for line in fp: 118 | print(line[:-1]) 119 | 120 | 121 | if __name__ == "__main__": 122 | log = TLog() 123 | 124 | Id = "E976E679968945779C095FE7FC56AE97" 125 | 126 | Id = log.uuid() 127 | 128 | log.write(Id, UnitState.created) 129 | log.write(Id, UnitState.executed, "python_function_module") 130 | 131 | print(Id in log) 132 | 133 | print(log[Id]) 134 | 135 | print() 136 | 137 | print(len(log)) 138 | 139 | log.dump() 140 | 141 | # log.remove() 142 | 143 | # print(len(log)) 144 | -------------------------------------------------------------------------------- /examples/clientIDoc.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | 6 | import pyrfc 7 | from pyrfc import ABAPApplicationError, ABAPRuntimeError, CommunicationError, LogonError 8 | 9 | 10 | def initial_screen(): 11 | choice = 9 12 | while choice not in [1, 2, 3, 4, 0]: 13 | print("\nPlease select one of the following choices:") 14 | print("\t1:\t Send a new IDoc (bgRFC, type 'T')") 15 | print("\t2:\t Send a new IDoc (bgRFC, type 'Q')") 16 | print("\t3:\t Send a new IDoc (tRFC)") 17 | print("\t4:\t Send a new IDoc (qRFC)") 18 | # print("\t2: Display current pending IDocs") 19 | print("\t0:\t Exit") 20 | try: 21 | choice = int(input("Enter choice: ")) 22 | except ValueError: 23 | choice = 0 24 | return choice 25 | 26 | 27 | def get_idoc_desc(idoc_id): 28 | idoc_control = { 29 | "TABNAM": "EDI_DC40", 30 | "MANDT": "000", 31 | "DOCNUM": "{0:016d}".format(idoc_id), 32 | "DIRECT": "2", 33 | "IDOCTYP": "TXTRAW01", 34 | "MESTYP": "TXTRAW", 35 | "SNDPRT": "LS", 36 | "SNDPRN": "SPJ_DEMO", 37 | "RCVPRT": "LS", 38 | "RCVPRN": "T90CLNT090", 39 | } 40 | idoc_data_dicts = [] 41 | for idx in range(1, idoc_id + 1): 42 | idoc_data = { 43 | "SEGNAM": "E1TXTRW", 44 | "MANDT": "000", 45 | "DOCNUM": "{0:016d}".format(idoc_id), 46 | "SEGNUM": "{0:06d}".format(idx), 47 | "SDATA": f"Some chars with a - {idoc_id + idx} - number!", 48 | } 49 | idoc_data_dicts.append(idoc_data) 50 | return { 51 | "IDOC_CONTROL_REC_40": [idoc_control], 52 | "IDOC_DATA_REC_40": idoc_data_dicts, 53 | } 54 | 55 | 56 | def main(): 57 | params_connection = {"dest": "MME"} 58 | 59 | idoc_id = 1 60 | 61 | try: 62 | connection = pyrfc.Connection(**params_connection) 63 | print(connection.alive) 64 | while True: 65 | choice = initial_screen() 66 | if 1 <= choice <= 4: # Create and send a new iDoc 67 | idoc = get_idoc_desc(idoc_id) 68 | print(f" - Created iDoc with idoc_id = {idoc_id}") 69 | idoc_id += 1 70 | if choice < 3: # bgRFC 71 | unit = connection.initialize_unit() 72 | print(f" - (bgRFC) Using unit id = {unit['id']}") 73 | else: # t/qRFC 74 | unit = connection.initialize_unit(background=False) 75 | print(f" - (t/qRFC) Using unit id = {unit['id']}") 76 | if choice == 2: # bgRFC, type 'Q' 77 | queue_input = input("Enter queue names (comma separated): ") 78 | queue_names = [qi.strip() for qi in queue_input.split(",")] 79 | connection.fill_and_submit_unit( 80 | unit, 81 | [("IDOC_INBOUND_ASYNCHRONOUS", idoc)], 82 | queue_names=queue_names, 83 | ) 84 | elif choice == 4: # qRFC 85 | queue_input = input("Enter queue name: ") 86 | queue = queue_input.strip() 87 | connection.fill_and_submit_unit( 88 | unit, 89 | [("IDOC_INBOUND_ASYNCHRONOUS", idoc)], 90 | queue_names=[queue], 91 | ) 92 | else: 93 | connection.fill_and_submit_unit( 94 | unit, 95 | [("IDOC_INBOUND_ASYNCHRONOUS", idoc)], 96 | ) 97 | print(" - Unit filled and submitted.") 98 | connection.confirm_unit(unit) 99 | print(" - Unit confirmed and destroyed.") 100 | else: 101 | print(" Bye...") 102 | break 103 | connection.close() 104 | 105 | except CommunicationError: 106 | print("Could not connect to server.") 107 | raise 108 | except LogonError: 109 | print("Could not log in. Wrong credentials?") 110 | raise 111 | except ( 112 | ABAPApplicationError, 113 | ABAPRuntimeError, 114 | ): 115 | print("An error occurred.") 116 | raise 117 | 118 | 119 | if __name__ == "__main__": 120 | main() 121 | -------------------------------------------------------------------------------- /tests/test_errors.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import pytest 6 | from pyrfc import ( 7 | ABAPApplicationError, 8 | Connection, 9 | ExternalRuntimeError, 10 | LogonError, 11 | RFCError, 12 | ) 13 | 14 | from tests.config import CONNECTION_PARAMS as params 15 | 16 | 17 | class TestErrors: 18 | def setup_method(self): 19 | self.conn = Connection(**params) 20 | assert self.conn.alive 21 | 22 | def teardown_method(self): 23 | self.conn.close() 24 | assert not self.conn.alive 25 | 26 | def test_no_connection_params(self): 27 | with pytest.raises(RFCError) as ex: 28 | Connection() 29 | error = ex.value 30 | assert error.args[0] == "Connection parameters missing" 31 | 32 | # todo: test correct status after error -> or to the error tests? 33 | def test_incomplete_params(self): 34 | incomplete_params = {} 35 | for pname in params: 36 | if pname not in ["ashost", "gwhost", "mshost"]: 37 | incomplete_params[pname] = params[pname] 38 | with pytest.raises(RFCError) as ex: 39 | Connection(**incomplete_params) 40 | error = ex.value 41 | assert error.code == 20 42 | assert error.key == "RFC_INVALID_PARAMETER" 43 | assert error.message in [ 44 | "Parameter ASHOST, GWHOST, MSHOST or SERVER_PORT is missing.", 45 | "Parameter ASHOST, GWHOST, MSHOST or PORT is missing.", 46 | "Parameter ASHOST, GWHOST or MSHOST is missing.", 47 | ] 48 | 49 | def test_denied_users(self): 50 | denied_params = params.copy() 51 | denied_params["user"] = "BLAFASEL" 52 | with pytest.raises(LogonError) as ex: 53 | Connection(**denied_params) 54 | error = ex.value 55 | assert error.code == 2 56 | assert error.key == "RFC_LOGON_FAILURE" 57 | assert error.message == "Name or password is incorrect (repeat logon)" 58 | 59 | def test_call_without_RFM_name(self): 60 | with pytest.raises(Exception) as ex: 61 | self.conn.call() 62 | error = ex.value 63 | assert isinstance(error, TypeError) is True 64 | assert error.args[0] == "call() takes at least 1 positional argument (0 given)" 65 | 66 | def test_call_non_existing_RFM(self): 67 | with pytest.raises(ABAPApplicationError) as ex: 68 | self.conn.call("undefined") 69 | error = ex.value 70 | assert error.code == 5 71 | assert error.key == "FU_NOT_FOUND" 72 | assert error.message == "ID:FL Type:E Number:046 undefined" 73 | 74 | def test_call_non_string_RFM_name(self): 75 | with pytest.raises(RFCError) as ex: 76 | self.conn.call(1) 77 | error = ex.value 78 | assert error.args == ( 79 | "Remote function module name must be unicode string, received:", 80 | 1, 81 | int, 82 | ) 83 | 84 | def test_call_non_existing_RFM_parameter(self): 85 | with pytest.raises(ExternalRuntimeError) as ex: 86 | self.conn.call("STFC_CONNECTION", undefined=0) 87 | error = ex.value 88 | assert error.code == 20 89 | assert error.key == "RFC_INVALID_PARAMETER" 90 | assert error.message == "field 'undefined' not found" 91 | 92 | def test_non_existing_field_structure(self): 93 | IMPORTSTRUCT = { 94 | "XRFCCHAR1": "A", 95 | "RFCCHAR2": "BC", 96 | "RFCCHAR4": "DEFG", 97 | } 98 | with pytest.raises(ExternalRuntimeError) as ex: 99 | self.conn.call("STFC_STRUCTURE", IMPORTSTRUCT=IMPORTSTRUCT) 100 | error = ex.value 101 | assert error.code == 20 102 | assert error.key == "RFC_INVALID_PARAMETER" 103 | assert error.message == "field 'XRFCCHAR1' not found" 104 | 105 | def test_non_existing_field_table(self): 106 | IMPORTSTRUCT = { 107 | "XRFCCHAR1": "A", 108 | "RFCCHAR2": "BC", 109 | "RFCCHAR4": "DEFG", 110 | } 111 | with pytest.raises(ExternalRuntimeError) as ex: 112 | self.conn.call("STFC_STRUCTURE", RFCTABLE=[IMPORTSTRUCT]) 113 | error = ex.value 114 | assert error.code == 20 115 | assert error.key == "RFC_INVALID_PARAMETER" 116 | assert error.message == "field 'XRFCCHAR1' not found" 117 | -------------------------------------------------------------------------------- /doc/intro.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: pyrfc 2 | 3 | .. _intro: 4 | 5 | ============ 6 | Introduction 7 | ============ 8 | 9 | 10 | The Python connector (a synonym for the :mod:`pyrfc` package) wraps the existing *SAP NW RFC Library*, 11 | often colloquially called *SAP C connector* or *SAP NW RFC SDK*. To start using :mod:`pyrfc` 12 | and similar connectors effectively, we highly recommend reading a series of insightful articles 13 | about RFC communication and *SAP NW RFC Library*, published in the SAP Professional Journal (SPJ), 14 | in 2009, by Ulrich Schmidt and Guangwei Li: *Improve communication between your C/C++ applications 15 | and SAP systems with SAP NetWeaver RFC SDK* 16 | `Part 1: RFC Client Programming `_, 17 | `Part 2: RFC Server Programming `_, 18 | `Part 3: Advanced Topics `_. 19 | 20 | The lecture of these articles and `NW RFC SDK Guide (SAP Help) `_ 21 | are recommended as an introduction into RFC communication and programming, while :mod:`pyrfc` documentation is 22 | focused merely on technical aspects of :mod:`pyrfc` API. 23 | 24 | 25 | Example usage 26 | ============= 27 | 28 | In order to call remote enabled ABAP function module, we need to open a connection with 29 | valid logon credentials. 30 | 31 | .. code-block:: python 32 | 33 | from pyrfc import Connection 34 | conn = Connection(user='me', passwd='secret', ashost='10.0.0.1', sysnr='00', client='100') 35 | 36 | Using an open connection we can call remote enabled ABAP function modules from Python. 37 | 38 | .. code-block:: python 39 | 40 | result = conn.call('STFC_CONNECTION', REQUTEXT=u'Hello SAP!') 41 | print result 42 | {u'ECHOTEXT': u'Hello SAP!', 43 | u'RESPTEXT': u'SAP R/3 Rel. 702 Sysid: ABC Date: 20121001 Time: 134524 Logon_Data: 100/ME/E'} 44 | 45 | Finally, the connection is closed automatically when the instance is deleted by the garbage collector. 46 | As this may take some time, we may either call the :meth:`~Connection.close` method explicitly 47 | or use the connection as a context manager: 48 | 49 | .. code-block:: python 50 | 51 | with Connection(user='me', ...) as conn: 52 | conn.call(...) 53 | # connection automatically closed here 54 | 55 | 56 | Functional coverage 57 | =================== 58 | 59 | The goal of the Python connector development was to provide a package for interacting with 60 | SAP ABAP systems on an intuitive and adequate abstract level. Not each and every available 61 | function provided by *SAP NW RFC Library* is therefore wrapped into Python, but classes and 62 | methods are implemented, covering the most of the use cases relevant for projects done so far. 63 | The drawback of this approach is that fine-grained RFC manipulation is not possible sometimes 64 | but coverage can be extended if needed. 65 | 66 | In line with this approach, we distinguish between two basic scenarios: 67 | 68 | * Client, Python client calls ABAP server 69 | * Server, ABAP client calls Python server 70 | 71 | The coverage is as follows: 72 | 73 | +------------------------------+---------------+--------------+ 74 | | | Client | Server (1) | 75 | +==============================+===============+==============+ 76 | | Standard functionality, | yes | no | 77 | | e.g. invoking arbitrary | | | 78 | | RFC | | | 79 | +------------------------------+---------------+--------------+ 80 | | Transactions (tRFC/qRFC) | yes | no | 81 | +------------------------------+---------------+--------------+ 82 | | Background RFC | yes (2) | no | 83 | +------------------------------+---------------+--------------+ 84 | | RFC Callbacks | no | no | 85 | +------------------------------+---------------+--------------+ 86 | | Secure network connect (SNC) | yes | no | 87 | +------------------------------+---------------+--------------+ 88 | | Single Sign on (SSO) | no | no | 89 | +------------------------------+---------------+--------------+ 90 | 91 | .. note:: 92 | (1) Server functionality is currently not implemented 93 | (2) Background RFC is currently not working. 94 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to PyRFC 2 | 3 | We're excited that you're interested in contributing to PyRFC! Your contribution can make this library even better. Please read the guidelines regarding contributions: 4 | 5 | - [Issues and Bugs](#issues-and-bugs) 6 | - [Feature Requests](#feature-requests) 7 | - [Contribute Code](#contribute-code) 8 | 9 | ## Issues and Bugs 10 | 11 | If you find a bug or some other issue with any part of the library, please [submit an issue](https://github.com/SAP/PyRFC/issues). Before doing so, please make sure that: 12 | 13 | - The issue is not a duplicate issue. 14 | - The issue has not been fixed in a newer release of the library. 15 | - The issue is reproducible. 16 | - Your explanation is clear and complete. 17 | - You provide example code and/or screenshots (recommended). 18 | 19 | If you meet the above criteria, you can submit issues with our [GitHub issue tracker](https://github.com/SAP/PyRFC/issues/new). Please use [labels](#usage-of-labels) to categorize your issue. 20 | 21 | ## Feature Requests 22 | 23 | You can also use the issue tracker to request a new feature or enhancement. Even if you want to implement the feature yourself, please first submit an issue detailing your proposal so that we may discuss it with you and the community before moving forward. Please use [labels](#usage-of-labels) when creating feature requests. 24 | 25 | ### Usage of Labels 26 | 27 | GitHub offers labels to categorize issues. You can use the following labels: 28 | 29 | Labels for issue categories: 30 | 31 | - bug: Something isn't working / Issues in the code. 32 | - documentation: Issues with the documentation (repo and website documentation). 33 | - enhancement: New feature or enhancement requests. 34 | 35 | Status of open issues: 36 | 37 | - (no label): The default status. 38 | - unconfirmed: The issue needs to be confirmed as being a bug or future enhancement. 39 | - approved: The issue is confirmed as being a bug to be fixed or enhancement to be developed. 40 | - author action: The issue's creator needs to provide additional information. 41 | - contribution welcome: The fix or enhancement is approved and you are invited to contribute to it. 42 | 43 | Status of closed issues: 44 | 45 | - fixed: A fix for the issue was provided. 46 | - duplicate: The issue is also reported in a different ticket and is being managed there. 47 | - invalid: The reported issue will not be addressed. 48 | - works: The issue cannot be reproduced, or the feature is working as expected. 49 | - wontfix: The issue will not be fixed. 50 | 51 | ## Contribute Code 52 | 53 | You are welcome to contribute code to PyRFC in order to fix issues or to add new features. 54 | 55 | ### Developer Certificate of Origin (DCO) 56 | 57 | Due to legal reasons, contributors will be asked to accept a DCO before submitting the first pull request to this project. This happens in an automated fashion during the submission process. SAP uses [the standard DCO text of the Linux Foundation](https://developercertificate.org/). 58 | 59 | ### Contribution Content Guidelines 60 | 61 | You must follow the coding style as best you can when submitting code. Take note of naming conventions, separation of concerns, and formatting rules. You can use the code formatter [Prettier](https://prettier.io/) to handle some of this for you automatically. 62 | 63 | ### Unit testing 64 | 65 | All components should have associated unit tests created with a code coverage score of at least 85%. Be an overachiever and shoot for 100% :) [Learn how to create unit tests](./UNIT-TESTING.md) 66 | 67 | ### How to contribute - the Process 68 | 69 | 1. Make sure the issue you've filed in the [issue tracker] has the label "contribution welcome" - otherwise, it is not ready to be worked on. 70 | 2. Fork the `PyRFC` repository to your GitHub account. 71 | 3. Create a branch for your issue or feature, and commit or push your changes on that branch. 72 | 4. Create a Pull Request from your forked repository to `github.com/SAP/PyRFC`. In the subject of the pull request, briefly describe the bug fix or enhancement you're contributing. In the pull request description, please provide a link to the issue in the issue tracker. 73 | 5. Follow the link posted by the CLA assistant to your pull request and accept it, as described above. 74 | 6. Wait for our code review and approval. We may ask you for additional commits, or make changes to your pull request ourselves. 75 | - Note that the PyRFC developers also have their regular duties so, depending on the required effort for reviewing, testing, and clarification, this may take a while. 76 | 7. Once the change has been approved, we inform you in a comment. 77 | 8. Your pull request cannot be merged directly into the branch (internal SAP processes), but is merged internally and immediately appears in the public repository as well. 78 | 9. We close the pull request. You can then delete the now obsolete branch. 79 | -------------------------------------------------------------------------------- /tests/test_function_group_stfc.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import datetime 6 | import unittest 7 | from typing import ClassVar 8 | 9 | from pyrfc import Connection 10 | 11 | from tests.config import CONNECTION_PARAMS as params 12 | 13 | 14 | class TestSTFC: 15 | """ 16 | This test cases cover selected functions from the STFC function group. 17 | """ 18 | 19 | def setup_method(self): 20 | self.conn = Connection(**params) 21 | assert self.conn.alive 22 | 23 | def test_info(self): 24 | connection_info = self.conn.get_connection_attributes() 25 | assert connection_info["isoLanguage"] == "EN" 26 | 27 | def teardown_method(self): 28 | self.conn.close() 29 | assert not self.conn.alive 30 | 31 | """ 32 | @unittest.skip("not supported yet (qrfc)") 33 | def test_STFC_CALL_QRFC(self): 34 | # STFC_CALL_QRFC qRFC mit Ausgangsqueue in der Verbuchung 35 | pass 36 | 37 | @unittest.skip("not supported yet (qrfc)") 38 | def test_STFC_CALL_QRFC_INBOUND(self): 39 | # STFC_CALL_QRFC_INBOUND qRFC mit Inboundqueue in der Verbuchung 40 | pass 41 | 42 | @unittest.skip("not supported yet (qrfc)") 43 | def test_STFC_CALL_QRFC_LOAD_TEST(self): 44 | # STFC_CALL_QRFC_LOAD_TEST qRFC mit Ausgangsqueue in der Verbuchung 45 | pass 46 | 47 | @unittest.skip("not supported yet (qrfc)") 48 | def test_STFC_CALL_QRFC_VB_PING(self): 49 | # STFC_CALL_QRFC_VB_PING Aufruf eines PING in Update Task 50 | pass 51 | 52 | @unittest.skip("not supported yet (trfc)") 53 | def test_STFC_CALL_TRFC(self): 54 | # STFC_CALL_TRFC tRFC in der Verbuchung 55 | pass 56 | 57 | # no remote-enabled module 58 | # def test_STFC_CALL_TRFC_PLUS_UPDATE(self): 59 | # STFC_CALL_TRFC_PLUS_UPDATE TRFC in VB innerhalb der VB nochmal tRFC 60 | # pass 61 | """ 62 | 63 | """ 64 | @unittest.skip("not supported yet (server)") 65 | def test_STFC_CONNECTION_BACK(self): 66 | # STFC_CONNECTION_BACK RFC-Test: CONNECTION Test 67 | pass 68 | 69 | @unittest.skip("not supported yet (??? VB specific)") 70 | def test_STFC_INSERT_INTO_TCPIC(self): 71 | # STFC_INSERT_INTO_TCPIC RFC-TEST: Insert data into TCPIC running in Update Task 72 | pass 73 | 74 | @unittest.skip("not supported yet (???)") 75 | def test_STFC_PERFORMANCE(self): 76 | # STFC_PERFORMANCE RFC-TEST: PERFORMANCE Test 77 | pass 78 | 79 | # This is not remote-enabled; however, we will test the expected error 80 | def test_STFC_PING_VB(self): 81 | pass 82 | # STFC_PING_VB RFC-Ping der in VB gerufen werden kann 83 | # with self.assertRaises(ABAPRuntimeError) as run: 84 | # self.conn.call('STFC_PING_VB') 85 | # self.assertEqual(run.exception.code, 3) 86 | # self.assertEqual(run.exception.key, 'CALL_FUNCTION_NOT_REMOTE') 87 | 88 | @unittest.skip("not supported yet (qrfc)") 89 | def test_STFC_QRFC_TCPIC(self): 90 | # STFC_QRFC_TCPIC 91 | # qRFC-Test: Wiederverwendung von qRFC mit Eingangsqueue 92 | pass 93 | 94 | @unittest.skip("not supported yet (qrfc/trfc)") 95 | def test_STFC_RETURN_DATA(self): 96 | # STFC_RETURN_DATA 97 | # RFC-TEST: tRFC/qRFC mit Rückmeldestatus und -daten 98 | pass 99 | 100 | @unittest.skip("not supported yet (qrfc/trfc)") 101 | def test_STFC_RETURN_DATA_INTERFACE(self): 102 | # STFC_RETURN_DATA_INTERFACE 103 | # RFC-TEST: Schnittstelenbeschreibung für tRFC/qRFC mit Rückmeldedaten 104 | pass 105 | """ 106 | 107 | """ 108 | @unittest.skip("not supported yet (server)") 109 | def test_STFC_START_CONNECT_REG_SERVER(self): 110 | # STFC_START_CONNECT_REG_SERVER 111 | # RFC-Test: CONNECTION Test 112 | pass 113 | """ 114 | 115 | # STFC_STRUCTURE Inhomogene Struktur 116 | imp: ClassVar[dict] = { 117 | "RFCFLOAT": 1.23456789, 118 | "RFCINT2": 0x7FFE, 119 | "RFCINT1": 0x7F, 120 | "RFCCHAR4": "bcde", 121 | "RFCINT4": 0x7FFFFFFE, 122 | "RFCHEX3": "fgh".encode("utf-8"), 123 | "RFCCHAR1": "a", 124 | "RFCCHAR2": "ij", 125 | "RFCTIME": "123456", # datetime.time(12,34,56), 126 | "RFCDATE": "20161231", # datetime.date(2011,10,17), 127 | "RFCDATA1": "k" * 50, 128 | "RFCDATA2": "l" * 50, 129 | } 130 | out: ClassVar[dict] = { 131 | "RFCFLOAT": imp["RFCFLOAT"] + 1, 132 | "RFCINT2": imp["RFCINT2"] + 1, 133 | "RFCINT1": imp["RFCINT1"] + 1, 134 | "RFCINT4": imp["RFCINT4"] + 1, 135 | "RFCHEX3": b"\xf1\xf2\xf3", 136 | "RFCCHAR1": "X", 137 | "RFCCHAR2": "YZ", 138 | "RFCDATE": str(datetime.date.today()).replace( 139 | "-", 140 | "", 141 | ), 142 | "RFCDATA1": "k" * 50, 143 | "RFCDATA2": "l" * 50, 144 | } 145 | 146 | 147 | if __name__ == "__main__": 148 | unittest.main() 149 | -------------------------------------------------------------------------------- /examples/server/z_nwrfc_server_bgrfc.abap: -------------------------------------------------------------------------------- 1 | *&---------------------------------------------------------------------* 2 | *& Report z_nwrfc_server_bgrfc 3 | *& 4 | *&---------------------------------------------------------------------* 5 | *& 6 | *& 7 | *&---------------------------------------------------------------------* 8 | 9 | report z_nwrfc_server_bgrfc. 10 | 11 | type-pools: abap. 12 | 13 | 14 | parameters: 15 | qname_o type qrfc_queue_name default 'BASIS_BGRFC_OUTIN', "#EC * 16 | qname_in type qrfc_queue_name default 'RFCSDK_BGRFC_OUTIN', "#EC * 17 | rfcdest type rfcdes-rfcdest default 'NWRFC_SERVER_OS', "#EC * 18 | n_unit type syindex default 1, "#EC * 19 | n_call type syindex default 1, "#EC * 20 | lock_out type abap_bool default abap_false, "#EC * 21 | lock_in type abap_bool default abap_true, "#EC * 22 | updtask type abap_bool default abap_false, "#EC * 23 | upd_loc type abap_bool default abap_false. "#EC * 24 | 25 | constants: 26 | lc_no_of_data_lines type i value 1, 27 | lc_unit_data type txline value '12345678901234567890123456789012'. 28 | 29 | data: 30 | l_queue_name type qrfc_queue_name, 31 | lt_queue_names type qrfc_queue_name_tab, 32 | l_unit_count type syindex, 33 | lt_tcpic type standard table of abaptext. 34 | 35 | data: 36 | l_dest type ref to if_bgrfc_destination_outbound, 37 | l_unit type ref to if_qrfc_unit_outinbound. 38 | 39 | data: 40 | l_lock_id_outbound type bgrfc_lock_id. "#EC NEEDED 41 | 42 | 43 | l_queue_name = qname_o. 44 | 45 | 46 | write: / text-003, 25 qname_o. 47 | write: / text-004, 25 qname_in. 48 | write: / text-005, 25 rfcdest. 49 | 50 | start-of-selection. 51 | * Fill TCPICDAT as required 52 | perform fill_tcpic_tab using lc_no_of_data_lines 53 | l_queue_name 54 | lc_unit_data 55 | changing lt_tcpic. 56 | 57 | * Create the destination object references 58 | try. 59 | l_dest = cl_bgrfc_destination_outbound=>create( rfcdest ). 60 | catch cx_bgrfc_invalid_destination. 61 | message e103(bgrfc) with rfcdest. 62 | 63 | endtry. 64 | 65 | do n_unit times. 66 | skip 1. 67 | 68 | * Activate local update task 69 | if upd_loc = abap_true. 70 | set update task local. 71 | endif. 72 | 73 | * Create new qRFC out-inbound unit 74 | l_unit = l_dest->create_qrfc_unit_outinbound( ). 75 | 76 | * Lock unit on Outbound and/or Inbound side 77 | if lock_out <> abap_false. 78 | l_lock_id_outbound = l_unit->lock( ). 79 | endif. 80 | 81 | if lock_in <> abap_false. 82 | l_unit->lock_at_inbound( ). 83 | endif. 84 | 85 | * Assign the unit to a queue 86 | * Outbound qname 87 | insert qname_o into table lt_queue_names. 88 | l_unit->add_queue_names_outbound( queue_names = lt_queue_names ). 89 | * Inbound qname 90 | clear lt_queue_names. 91 | insert qname_in into table lt_queue_names. 92 | l_unit->add_queue_names_inbound( queue_names = lt_queue_names ). 93 | 94 | * Using UPDATE TASK, called once per unit 95 | if ( abap_true = updtask ). 96 | call function 'CALL_V1_PING' in update task. 97 | endif. 98 | 99 | * Output 100 | l_unit_count = sy-index. 101 | write: / text-001, 30 l_unit_count. 102 | write: / text-002, 30 n_call. 103 | 104 | do n_call times. 105 | 106 | call function 'STFC_WRITE_TO_TCPIC' 107 | in background unit l_unit 108 | tables 109 | tcpicdat = lt_tcpic. 110 | 111 | enddo. 112 | commit work. 113 | enddo. 114 | 115 | * Additional information 116 | skip 1. 117 | if lock_out <> abap_false. 118 | write: /'Outbound-Unit lock set!'. "#EC NOTEXT 119 | endif. 120 | 121 | if lock_in <> abap_false. 122 | write: /'Inbound-Unit lock set!'. "#EC NOTEXT 123 | endif. 124 | 125 | if ( abap_true = updtask ). 126 | write: /'Unit called together with update task!'. "#EC NOTEXT 127 | endif. 128 | 129 | 130 | *&--------------------------------------------------------------------* 131 | *& Form FILL_TCPIC_TAB 132 | *&--------------------------------------------------------------------* 133 | * text 134 | *---------------------------------------------------------------------* 135 | * -->l_no_of_data_lines text 136 | * -->l_queue_name text 137 | * -->l_unit_data text 138 | * -->lt_tcpic text 139 | *---------------------------------------------------------------------* 140 | form fill_tcpic_tab using l_no_of_data_lines type i 141 | l_queue_name type qrfc_queue_name 142 | l_unit_data type txline 143 | changing lt_tcpic type index table. 144 | 145 | data: 146 | l_tcpic type abaptext, 147 | l_line_nr(5) type n value 0. 148 | 149 | if ( 0 = l_no_of_data_lines ). 150 | return. 151 | endif. 152 | 153 | clear lt_tcpic. 154 | 155 | l_tcpic = l_queue_name. 156 | l_tcpic+40(32) = l_unit_data. 157 | 158 | do l_no_of_data_lines times. 159 | add 1 to l_line_nr. 160 | l_tcpic+20(6) = l_line_nr. 161 | 162 | append l_tcpic to lt_tcpic. 163 | 164 | enddo. 165 | 166 | endform. "FILL_TCPIC_TAB -------------------------------------------------------------------------------- /doc/authentication.rst: -------------------------------------------------------------------------------- 1 | .. _authentication: 2 | 3 | ======== 4 | Security 5 | ======== 6 | 7 | Plain RFC connections are mainly used for prototyping, while in production 8 | secure connections are required. For more information on RFC security see: 9 | 10 | * `Security on SAP Service Marketplace `_ 11 | * `RFC Security Best Practices on SAP SCN `_ 12 | * `Securing RFC Connections on ABAP Connectivity Wiki `_ 13 | 14 | SAP NW RFC Library supports plain and secure connection with following authentication methods: 15 | 16 | * `Plain with user / password <#plain-auth>`_ 17 | 18 | * SNC 19 | 20 | * `with User PSE <#snc-with-user-pse>`_ 21 | * `with X509 <#snc-with-x509>`_ 22 | 23 | NW ABAP servers support in addition: 24 | 25 | * SAP logon tickets 26 | * Security Assertion Markup Language (SAML) 27 | 28 | Assuming you are familiar with abovementioned concepts and have ABAP backend system 29 | configured for SNC communication, here you may find connection strings examples, 30 | for testing plain and secure RFC connections, with various authentication methods. 31 | 32 | 33 | Authentication 34 | ============== 35 | 36 | .. _plain-auth: 37 | 38 | Plain with user / password 39 | -------------------------- 40 | 41 | The simplest and least secure form of the user authentication. 42 | 43 | .. code-block:: python 44 | 45 | ABAP_SYSTEM = { 46 | 'user': 'demo', 47 | 'passwd': 'welcome', 48 | 49 | 'name': 'I64', 50 | 'client': '800', 51 | 'ashost': '10.0.0.1', 52 | 'sysnr': '00', 53 | 'saprouter': SAPROUTER, 54 | 'trace': '3' 55 | } 56 | 57 | c = get_connection(ABAP_SYSTEM) # plain 58 | 59 | .. _snc-with-user-pse: 60 | 61 | SNC with User PSE 62 | ----------------- 63 | 64 | `User PSE `_ 65 | is used for opening the SNC connection and the same user is used for the authentication 66 | (logon) in NW ABAP backend. Generally not recomended, see `SAP Note 1028503 - SNC-secured RFC connection: Logon ticket is ignored `_ 67 | 68 | **Prerequisites** 69 | 70 | * SNC name must be configured for the ABAP user in NW ABAP system, using transaction SU01 71 | 72 | .. image:: _static/images/SU01-SNC.png 73 | :align: center 74 | 75 | * SAP Single Sign On must be configured on a client and the user must be logged in on a client. 76 | 77 | .. code-block:: python 78 | 79 | ABAP_SYSTEM = { 80 | 'snc_partnername': 'p:CN=I64, O=SAP-AG, C=DE', 81 | 'snc_lib': 'C:\\Program Files (x86)\\SECUDE\\OfficeSecurity\\secude.dll', 82 | 83 | 'name': 'I64', 84 | 'client': '800', 85 | 'ashost': '10.0.0.1', 86 | 'sysnr': '00', 87 | 'saprouter': SAPROUTER, 88 | 'trace': '3' 89 | } 90 | 91 | c = get_connection(ABAP_SYSTEM) 92 | 93 | In this example the SNC_LIB key contains the path to security library 94 | (SAP cryptographic library or 3rd party product). Alternatively, the 95 | SNC_LIB can be set as the environment variable, in which case it does 96 | not have to be provided as a parameter for opening SNC connection. 97 | 98 | .. _snc-with-x50: 99 | 100 | SNC with X509 101 | ------------- 102 | 103 | The client system PSE is used for opening SNC connection and forwarding user 104 | X509 certificate to NW ABAP backend system, for authentication and logon. 105 | 106 | **Prerequisites** 107 | 108 | * The user does not have to be logged into the client system, neither the Single 109 | Sign On must be configured on a client 110 | * The trusted relationship must be established between the NW ABAP backend and 111 | the client system. 112 | * The client system must be registered in the NW ABAP backend Access Control 113 | List (ACL), using transaction SNC0 114 | * Keystores are generated on a client system, using SAP cryptography tool *SAPGENPSE* and 115 | the environment variable SECUDIR points to the folder with generated keystores 116 | 117 | .. image:: _static/images/SNC0-1.png 118 | :align: center 119 | 120 | .. image:: _static/images/SNC0-2.png 121 | :align: center 122 | 123 | * User X509 certificate must be mapped to ABAP NW backend user, using transaction EXTID_DN 124 | 125 | .. image:: _static/images/EXTID_DN-1.png 126 | :align: center 127 | 128 | .. image:: _static/images/EXTID_DN-2.png 129 | :align: center 130 | 131 | The same connection parameters as in a previous example, with X509 certificate added. 132 | 133 | .. code-block:: python 134 | 135 | ABAP_SYSTEM = { 136 | 'snc_partnername': 'p:CN=I64, O=SAP-AG, C=DE', 137 | 'snc_lib': 'C:\\Program Files (x86)\\SECUDE\\OfficeSecurity\\secude.dll', 138 | 139 | 'x509cert': 'MIIDJjCCAtCgAwIBAgIBNzA ... NgalgcTJf3iUjZ1e5Iv5PLKO', 140 | 141 | 'name': 'I64', 142 | 'client': '800', 143 | 'ashost': '10.0.0.1', 144 | 'sysnr': '00', 145 | 'saprouter': SAPROUTER, 146 | 'trace': '3' 147 | } 148 | 149 | c = get_connection(ABAP_SYSTEM) 150 | 151 | See `SAP Help `_ for more information. 152 | -------------------------------------------------------------------------------- /tests/test_client_config.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import datetime 6 | 7 | import pytest 8 | from pyrfc import Connection, RFCError 9 | 10 | from tests.config import CONNECTION_PARAMS 11 | 12 | 13 | class TestConnection: 14 | def test_config_rstrip_false(self): 15 | conn = Connection( 16 | config={"rstrip": False}, 17 | **CONNECTION_PARAMS, 18 | ) 19 | 20 | # Test with rstrip=False (input length=255 char) 21 | hello = "Hällo SAP!" + " " * 245 22 | res = conn.call( 23 | "STFC_CONNECTION", 24 | REQUTEXT=hello, 25 | ) 26 | assert res["ECHOTEXT"] == hello 27 | 28 | # Test with rstrip=False (input length=10 char) 29 | res = conn.call( 30 | "STFC_CONNECTION", 31 | REQUTEXT=hello.rstrip(), 32 | ) 33 | assert res["ECHOTEXT"] == hello 34 | conn.close() 35 | 36 | def test_config_rstrip_true(self): 37 | conn = Connection(**CONNECTION_PARAMS) 38 | 39 | # Test with rstrip=True (input length=255 char) 40 | hello = "Hällo SAP!" + " " * 245 41 | res = conn.call( 42 | "STFC_CONNECTION", 43 | REQUTEXT=hello, 44 | ) 45 | assert res["ECHOTEXT"] == hello.rstrip() 46 | 47 | # Test with rstrip=True (input length=10 char) 48 | res = conn.call( 49 | "STFC_CONNECTION", 50 | REQUTEXT=hello.rstrip(), 51 | ) 52 | assert res["ECHOTEXT"] == hello.rstrip() 53 | conn.close() 54 | 55 | def test_config_dtime_true(self): 56 | conn = Connection( 57 | config={"dtime": True}, 58 | **CONNECTION_PARAMS, 59 | ) 60 | # dates as datetime objects 61 | dates = conn.call( 62 | "BAPI_USER_GET_DETAIL", 63 | USERNAME="demo", 64 | )["LASTMODIFIED"] 65 | assert type(dates["MODDATE"]) is datetime.date 66 | assert type(dates["MODTIME"]) is datetime.time 67 | conn.close() 68 | 69 | def test_config_dtime_false(self): 70 | conn = Connection(**CONNECTION_PARAMS) 71 | # dates as strings 72 | dates = conn.call( 73 | "BAPI_USER_GET_DETAIL", 74 | USERNAME="demo", 75 | )["LASTMODIFIED"] 76 | assert type(dates["MODDATE"]) is not datetime.date 77 | assert type(dates["MODDATE"]) is not datetime.time 78 | conn.close() 79 | 80 | def test_config_return_import_params_false(self): 81 | # no import params return 82 | conn = Connection(**CONNECTION_PARAMS) 83 | hello = "Hällo SAP!" 84 | res = conn.call( 85 | "STFC_CONNECTION", 86 | REQUTEXT=hello, 87 | ) 88 | assert "REQTEXT" not in res 89 | conn.close() 90 | 91 | def test_config_return_import_params_true(self): 92 | # return import params 93 | conn = Connection( 94 | config={"return_import_params": True}, 95 | **CONNECTION_PARAMS, 96 | ) 97 | res = conn.call( 98 | "STFC_CONNECTION", 99 | REQUTEXT="hello", 100 | ) 101 | assert "REQUTEXT" in res 102 | conn.close() 103 | 104 | def test_config_timeout(self): 105 | conn = Connection( 106 | config={"timeout": 123}, 107 | **CONNECTION_PARAMS, 108 | ) 109 | assert conn.options["timeout"] == 123 110 | conn.close() 111 | 112 | def test_config_not_supported(self): 113 | with pytest.raises(RFCError) as ex: 114 | Connection( 115 | config={"xtimeout": 123}, 116 | **CONNECTION_PARAMS, 117 | ) 118 | error = ex.value 119 | assert ( 120 | error.args[0] 121 | == "Connection configuration option 'xtimeout' is not supported" 122 | ) 123 | 124 | def test_config_parameter(self): 125 | conn = Connection( 126 | config={"dtime": True}, 127 | **CONNECTION_PARAMS, 128 | ) 129 | # dtime test 130 | dates = conn.call( 131 | "BAPI_USER_GET_DETAIL", 132 | USERNAME="demo", 133 | )["LASTMODIFIED"] 134 | assert type(dates["MODDATE"]) is datetime.date 135 | assert type(dates["MODTIME"]) is datetime.time 136 | conn.close() 137 | conn = Connection(**CONNECTION_PARAMS) 138 | dates = conn.call( 139 | "BAPI_USER_GET_DETAIL", 140 | USERNAME="demo", 141 | )["LASTMODIFIED"] 142 | assert type(dates["MODDATE"]) is not datetime.date 143 | assert type(dates["MODDATE"]) is not datetime.time 144 | conn.close() 145 | conn = Connection( 146 | config={"dtime": True}, 147 | **CONNECTION_PARAMS, 148 | ) 149 | # no import params return 150 | hello = "Hällo SAP!" 151 | res = conn.call( 152 | "STFC_CONNECTION", 153 | REQUTEXT=hello, 154 | ) 155 | assert "REQTEXT" not in res 156 | conn.close() 157 | # return import params 158 | conn = Connection( 159 | config={"return_import_params": True}, 160 | **CONNECTION_PARAMS, 161 | ) 162 | res = conn.call( 163 | "STFC_CONNECTION", 164 | REQUTEXT=hello.rstrip(), 165 | ) 166 | assert hello.rstrip() == res["REQUTEXT"] 167 | conn.close() 168 | -------------------------------------------------------------------------------- /tests/test_throughput.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import pytest 6 | from pyrfc import Connection, Throughput 7 | 8 | from tests.config import CONNECTION_DEST as params 9 | 10 | 11 | def equal_no_time(t1, t2): 12 | counters = [ 13 | "numberOfCalls", 14 | "sentBytes", 15 | "receivedBytes", 16 | # "applicationTime", 17 | # "totalTime", 18 | # "serializationTime", 19 | # "deserializationTime", 20 | ] 21 | for cnt in counters: 22 | if t1[cnt] != t2[cnt]: 23 | return (cnt, t1[cnt], t2[cnt]) 24 | return True 25 | 26 | 27 | class TestThroughput: 28 | def test_create_without_connection(self): 29 | throughput = Throughput() 30 | assert len(throughput.connections) == 0 31 | 32 | def test_create_with_single_connection(self): 33 | conn = Connection(**params) 34 | throughput = Throughput(conn) 35 | assert len(throughput.connections) == 1 36 | conn.close() 37 | 38 | def test_create_with_multiple_connection(self): 39 | c1 = Connection(**params) 40 | c2 = Connection(**params) 41 | throughput = Throughput([c1, c2]) 42 | assert len(throughput.connections) == 2 43 | throughput = Throughput([c1, c1]) 44 | assert len(throughput.connections) == 1 45 | c1.close() 46 | c2.close() 47 | 48 | def test_remove_from_connection(self): 49 | c1 = Connection(**params) 50 | c2 = Connection(**params) 51 | throughput = Throughput([c1, c2]) 52 | assert len(throughput.connections) == 2 53 | throughput.removeFromConnection(c2) 54 | assert len(throughput.connections) == 1 55 | assert next(iter(throughput.connections)) == c1 56 | c1.close() 57 | c2.close() 58 | 59 | def test_create_with_invalid_connection(self): 60 | conn = Connection(**params) 61 | with pytest.raises(Exception) as ex: 62 | Throughput(1) 63 | error = ex.value 64 | assert isinstance(error, TypeError) is True 65 | assert error.args == ( 66 | "Connection object required, received", 67 | 1, 68 | "of type", 69 | type(1), 70 | ) 71 | 72 | with pytest.raises(Exception) as ex: 73 | Throughput([conn, 1]) 74 | error = ex.value 75 | assert isinstance(error, TypeError) is True 76 | assert error.args == ( 77 | "Connection object required, received", 78 | 1, 79 | "of type", 80 | type(1), 81 | ) 82 | conn.close() 83 | 84 | def test_get_from_connection(self): 85 | c1 = Connection(**params) 86 | c2 = Connection(**params) 87 | 88 | t_c1 = Throughput(c1) 89 | t_c2 = Throughput(c2) 90 | t_from_c = Throughput.getFromConnection(c1) 91 | assert t_from_c == t_c1 92 | t_from_c = Throughput.getFromConnection(c2) 93 | assert t_from_c == t_c2 94 | c1.close() 95 | c2.close() 96 | 97 | def test_throutput_single_connection(self): 98 | conn = Connection(**params) 99 | assert conn.alive 100 | 101 | throughput = Throughput() 102 | throughput.setOnConnection(conn) 103 | assert throughput.stats == { 104 | "numberOfCalls": 0, 105 | "sentBytes": 0, 106 | "receivedBytes": 0, 107 | "applicationTime": 0, 108 | "totalTime": 0, 109 | "serializationTime": 0, 110 | "deserializationTime": 0, 111 | } 112 | 113 | conn.call("STFC_CONNECTION", REQUTEXT="hello") 114 | assert equal_no_time( 115 | throughput.stats, 116 | { 117 | "numberOfCalls": 2, 118 | "sentBytes": 1089, 119 | "receivedBytes": 2812, 120 | "applicationTime": 0, 121 | "totalTime": 387, 122 | "serializationTime": 0, 123 | "deserializationTime": 0, 124 | }, 125 | ) 126 | 127 | conn.call("STFC_CONNECTION", REQUTEXT="hello") 128 | assert equal_no_time( 129 | throughput.stats, 130 | { 131 | "numberOfCalls": 3, 132 | "sentBytes": 1737, 133 | "receivedBytes": 4022, 134 | "applicationTime": 0, 135 | "totalTime": 410, 136 | "serializationTime": 0, 137 | "deserializationTime": 0, 138 | }, 139 | ) 140 | 141 | throughput.reset() 142 | assert equal_no_time( 143 | throughput.stats, 144 | { 145 | "numberOfCalls": 0, 146 | "sentBytes": 0, 147 | "receivedBytes": 0, 148 | "applicationTime": 0, 149 | "totalTime": 0, 150 | "serializationTime": 0, 151 | "deserializationTime": 0, 152 | }, 153 | ) 154 | 155 | conn.call("BAPI_USER_GET_DETAIL", USERNAME="demo") 156 | assert equal_no_time( 157 | throughput.stats, 158 | { 159 | "numberOfCalls": 90, 160 | "sentBytes": 67028, 161 | "receivedBytes": 416456, 162 | "applicationTime": 0, 163 | "totalTime": 4833, 164 | "serializationTime": 2, 165 | "deserializationTime": 32, 166 | }, 167 | ) 168 | 169 | conn.close() 170 | assert not conn.alive 171 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # SAP Open Source Code of Conduct 2 | 3 | SAP adopts the [Contributor's Covenant 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) 4 | across our open source projects to ensure a welcoming and open culture for everyone involved. 5 | 6 | ## Our Pledge 7 | 8 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. 9 | 10 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to a positive environment for our community include: 15 | 16 | * Demonstrating empathy and kindness toward other people 17 | * Being respectful of differing opinions, viewpoints, and experiences 18 | * Giving and gracefully accepting constructive feedback 19 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 20 | * Focusing on what is best not just for us as individuals, but for the overall community 21 | 22 | Examples of unacceptable behavior include: 23 | 24 | * The use of sexualized language or imagery, and sexual attention or advances of any kind 25 | * Trolling, insulting or derogatory comments, and personal or political attacks 26 | * Public or private harassment 27 | * Publishing others' private information, such as a physical or email address, without their explicit permission 28 | * Other conduct which could reasonably be considered inappropriate in a professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [ospo@sap.com](mailto:ospo@sap.com) (SAP Open Source Program Office). All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 77 | 78 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 79 | 80 | For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. 81 | 82 | [homepage]: https://www.contributor-covenant.org 83 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 84 | [Mozilla CoC]: https://github.com/mozilla/diversity 85 | [FAQ]: https://www.contributor-covenant.org/faq 86 | [translations]: https://www.contributor-covenant.org/translations -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import inspect 6 | import os 7 | import sys 8 | 9 | from setuptools import Extension, setup 10 | 11 | PACKAGE_NAME = "pyrfc" 12 | MODULE_NAME = "_cyrfc" 13 | 14 | build_cython = sys.platform.startswith("linux") or bool( 15 | os.environ.get("PYRFC_BUILD_CYTHON") 16 | ) 17 | 18 | # build from source on Linux 19 | CMDCLASS = {} 20 | if build_cython: 21 | try: 22 | from Cython.Build import cythonize 23 | from Cython.Distutils import build_ext 24 | except ImportError: 25 | sys.exit("Cython not installed") 26 | CMDCLASS = {"build_ext": build_ext} 27 | 28 | # check if SAP NWRFC SDK configured 29 | SAPNWRFC_HOME = os.environ.get("SAPNWRFC_HOME") 30 | if not SAPNWRFC_HOME: 31 | sys.exit( 32 | "Environment variable SAPNWRFC_HOME not set.\n" 33 | "This variable shall point to the root directory of the SAP NWRFC Library." 34 | ) 35 | 36 | 37 | # Build options for supported wheels 38 | if sys.platform.startswith("linux"): 39 | LIBS = ["sapnwrfc", "sapucum"] 40 | MACROS = [ 41 | ("NDEBUG", None), 42 | ("_LARGEFILE_SOURCE", None), 43 | ("_CONSOLE", None), 44 | ("_FILE_OFFSET_BITS", 64), 45 | ("SAPonUNIX", None), 46 | ("SAPwithUNICODE", None), 47 | ("SAPwithTHREADS", None), 48 | ("SAPonLIN", None), 49 | ] 50 | COMPILE_ARGS = [ 51 | "-Wall", 52 | "-O2", 53 | "-fexceptions", 54 | "-funsigned-char", 55 | "-fno-strict-aliasing", 56 | "-Wall", 57 | "-Wno-uninitialized", 58 | "-Wno-deprecated-declarations", 59 | "-Wno-unused-function", 60 | "-Wcast-align", 61 | "-fPIC", 62 | "-pthread", 63 | "-minline-all-stringops", 64 | f"-I{SAPNWRFC_HOME}/include", 65 | ] 66 | LINK_ARGS = [f"-L{SAPNWRFC_HOME}/lib"] 67 | elif sys.platform.startswith("win"): 68 | # https://docs.microsoft.com/en-us/cpp/build/reference/compiler-options-listed-alphabetically 69 | 70 | # Python sources 71 | PYTHONSOURCE = os.getenv( 72 | "PYTHONSOURCE", inspect.getfile(inspect).split("/inspect.py")[0] 73 | ) 74 | if not PYTHONSOURCE: 75 | sys.exit("Environment variable PYTHONSOURCE not set.") 76 | 77 | LIBS = ["sapnwrfc", "libsapucum"] 78 | 79 | MACROS = [ 80 | ("SAPonNT", None), 81 | ("_CRT_NON_CONFORMING_SWPRINTFS", None), 82 | ("_CRT_SECURE_NO_DEPRECATES", None), 83 | ("_CRT_NONSTDC_NO_DEPRECATE", None), 84 | ("_AFXDLL", None), 85 | ("WIN32", None), 86 | ("_WIN32_WINNT", "0x0502"), 87 | ("WIN64", None), 88 | ("_AMD64_", None), 89 | ("NDEBUG", None), 90 | ("SAPwithUNICODE", None), 91 | ("UNICODE", None), 92 | ("_UNICODE", None), 93 | ("SAPwithTHREADS", None), 94 | ("_ATL_ALLOW_CHAR_UNSIGNED", None), 95 | ("_LARGEFILE_SOURCE", None), 96 | ("_CONSOLE", None), 97 | ("SAP_PLATFORM_MAKENAME", "ntintel"), 98 | ] 99 | 100 | COMPILE_ARGS = [ 101 | f"-I{SAPNWRFC_HOME}\\include", 102 | f"-I{PYTHONSOURCE}\\Include", 103 | f"-I{PYTHONSOURCE}\\Include\\PC", 104 | "/EHs", 105 | "/GL", 106 | "/Gy", 107 | "/J", 108 | "/MD", 109 | "/nologo", 110 | "/O2", 111 | "/Oy-", 112 | "/we4552", 113 | "/we4700", 114 | "/we4789", 115 | "/W3", 116 | "/Z7", 117 | ] 118 | 119 | LINK_ARGS = [ 120 | f"-LIBPATH:{SAPNWRFC_HOME}\\lib", 121 | f"-LIBPATH:{PYTHONSOURCE}\\PCbuild", 122 | "/NXCOMPAT", 123 | "/STACK:0x2000000", 124 | "/SWAPRUN:NET", 125 | "/DEBUG", 126 | "/OPT:REF", 127 | "/DEBUGTYPE:CV,FIXUP", 128 | "/MACHINE:amd64", 129 | "/nologo", 130 | "/LTCG", 131 | ] 132 | elif sys.platform.startswith("darwin"): 133 | MACOS_VERSION_MIN = "11" 134 | 135 | LIBS = ["sapnwrfc", "sapucum"] 136 | MACROS = [ 137 | ("NDEBUG", None), 138 | ("_LARGEFILE_SOURCE", None), 139 | ("_CONSOLE", None), 140 | ("_FILE_OFFSET_BITS", 64), 141 | ("SAPonUNIX", None), 142 | ("SAPwithUNICODE", None), 143 | ("SAPwithTHREADS", None), 144 | ("SAPonDARW", None), 145 | ] 146 | COMPILE_ARGS = [ 147 | "-Wall", 148 | "-O2", 149 | "-fexceptions", 150 | "-funsigned-char", 151 | "-fno-strict-aliasing", 152 | "-Wno-uninitialized", 153 | "-Wcast-align", 154 | "-fPIC", 155 | "-pthread", 156 | "-minline-all-stringops", 157 | "-isystem", 158 | "-std=c++11", 159 | f"-mmacosx-version-min={MACOS_VERSION_MIN}", 160 | f"-I{SAPNWRFC_HOME}/include", 161 | "-Wno-cast-align", 162 | "-Wno-deprecated-declarations", 163 | "-Wno-unused-function", 164 | "-Wno-nullability-completeness", 165 | "-Wno-expansion-to-defined", 166 | "-Wno-unreachable-code", 167 | "-Wno-unreachable-code-fallthrough", 168 | ] 169 | LINK_ARGS = [ 170 | f"-L{SAPNWRFC_HOME}/lib", 171 | "-stdlib=libc++", 172 | f"-mmacosx-version-min={MACOS_VERSION_MIN}", 173 | f"-Wl,-rpath,{SAPNWRFC_HOME}/lib", 174 | ] 175 | else: 176 | sys.exit(f"Platform not supported: {sys.platform}.") 177 | 178 | 179 | PYRFC_EXT = Extension( 180 | language="c++", 181 | name=f"{PACKAGE_NAME}.{MODULE_NAME}", 182 | sources=[f"src/{PACKAGE_NAME}/{MODULE_NAME}.pyx"], 183 | define_macros=MACROS, 184 | extra_compile_args=COMPILE_ARGS, 185 | extra_link_args=LINK_ARGS, 186 | libraries=LIBS, 187 | ) 188 | 189 | PYRFC_EXT.cython_directives = {"language_level": "3", "embedsignature": True} # type: ignore 190 | 191 | setup( 192 | name=PACKAGE_NAME, 193 | cmdclass=CMDCLASS, 194 | ext_modules=cythonize(PYRFC_EXT, annotate=True) if build_cython else [PYRFC_EXT], 195 | test_suite="tests", 196 | ) 197 | -------------------------------------------------------------------------------- /examples/server/bgrfc_server.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import sys 6 | 7 | from backends import BACKEND 8 | from pyrfc import RCStatus, Server, UnitCallType, UnitState 9 | from tlog import TLog 10 | 11 | backend_dest = sys.argv[1] 12 | 13 | tlog = TLog() 14 | 15 | # clear the log 16 | tlog.remove() 17 | 18 | 19 | def stfc_write_to_tcpic(request_context, RESTART_QNAME="", TCPICDAT=[]): 20 | try: 21 | context = request_context["server_context"] 22 | tid = context["unit_identifier"]["id"] if "unit_identifier" in context else None 23 | print( 24 | "server function: stfc_write_to_tcpic", 25 | "tid:", 26 | tid, 27 | "call:", 28 | UnitCallType.synchronous.value, 29 | context, 30 | ) 31 | 32 | # if context["call_type"] != UnitCallType.synchronous: 33 | # # Obtain the database connection that was opened for the 34 | # # current TID context.tid in the onCheckTID handler. 35 | 36 | # Implement functionality of function module MY_FUNCTION_MODULE, if any. 37 | # print(f"RESTART_QNAME: {RESTART_QNAME}") 38 | print("TCPICDAT:", TCPICDAT) 39 | 40 | # try: 41 | # unit = client.initialize_unit() 42 | # print("unit:", unit) 43 | # client.fill_and_submit_unit( 44 | # unit, 45 | # [("STFC_WRITE_TO_TCPIC", {"TCPICDAT": TCPICDAT})], 46 | # queue_names=["RFCSDK_QUEUE_IN"], 47 | # attributes={"lock": 1}, 48 | # ) 49 | # except Exception as ex: 50 | # print(ex) 51 | 52 | if context["call_type"] != UnitCallType.synchronous: # SIM102 53 | # Store the data received in funcHandle as well as any results 54 | # computed in the business functionality of the FM (if any) 55 | # via the DB connection obtained above. 56 | # Optionally, if you want to make status tracking and forensic analysis 57 | # in case of an error easier, you can update the TID`s status to EXECUTED 58 | # in this step. 59 | if tid is not None: 60 | tlog.write(tid, UnitState.executed, "stfc_write_to_tcpic") 61 | return RCStatus.OK 62 | except Exception as ex: 63 | print("Error in stfc_write_to_tcpic", ex) 64 | return RCStatus.RFC_EXTERNAL_FAILURE 65 | 66 | 67 | def on_auth_check(func_name=False, request_context=None): 68 | if request_context is None: 69 | request_context = {} 70 | print(f"authorization check for '{func_name}'") 71 | # print("request_context", request_context) 72 | return RCStatus.OK 73 | 74 | 75 | def onCheckFunction( 76 | rfcHandle, 77 | unit_identifier, 78 | ): 79 | # section 5.5.1 pg 76 of SAP NWRFC SDK Programming Guide 7.50 80 | if tlog is None: 81 | # TLog not available 82 | return RCStatus.RFC_EXTERNAL_FAILURE 83 | tid = unit_identifier["id"] 84 | tlog_record = tlog[tid] 85 | if tlog_record is None: 86 | # Create TID record 87 | tlog_record = tlog.write(tid, UnitState.created) 88 | print( 89 | "bgRFC:onCheck", 90 | f"handle {rfcHandle} tid {tid} status {tlog_record['status']}", 91 | ) 92 | if tlog_record["status"] in [UnitState.created.name, UnitState.rolled_back.name]: 93 | return RCStatus.OK 94 | else: 95 | return RCStatus.RFC_EXECUTED 96 | 97 | 98 | def onCommitFunction(rfcHandle, unit_identifier): 99 | # section 5.5.3 pg 79 of SAP NWRFC SDK Programming Guide 7.50 100 | print("bgRFC:onCommit handle", rfcHandle, "unit", unit_identifier) 101 | 102 | if tlog is None: 103 | # TLog not available 104 | return RCStatus.RFC_EXTERNAL_FAILURE 105 | tid = unit_identifier["id"] 106 | tlog.write(tid, UnitState.committed) 107 | 108 | return RCStatus.OK 109 | 110 | 111 | def onConfirmFunction(rfcHandle, unit_identifier): 112 | # section 5.5.4 pg 80 of SAP NWRFC SDK Programming Guide 7.50 113 | print("bgRFC:onConfirm handle", rfcHandle, "unit", unit_identifier) 114 | 115 | if tlog is None: 116 | # TLog not available 117 | return RCStatus.RFC_EXTERNAL_FAILURE 118 | 119 | tid = unit_identifier["id"] 120 | tlog.write(tid, UnitState.confirmed) 121 | # or remove tid records from db 122 | # tlog.remove(tid) 123 | 124 | return RCStatus.OK 125 | 126 | 127 | def onRollbackFunction(rfcHandle, unit_identifier): 128 | # section 5.5.3 pg 80 of SAP NWRFC SDK Programming Guide 7.50 129 | print("bgRFC:onRollback handle", rfcHandle, "unit", unit_identifier) 130 | 131 | if tlog is None: 132 | # TLog not available 133 | return RCStatus.RFC_EXTERNAL_FAILURE 134 | 135 | tid = unit_identifier["id"] 136 | tlog.write(tid, UnitState.rolled_back) 137 | 138 | return RCStatus.RFC_OK 139 | 140 | 141 | def onGetStateFunction(rfcHandle, unit_identifier): 142 | # section 5.6.3 pg 84 of SAP NWRFC SDK Programming Guide 7.50 143 | print("bgRFC:onGetState handle", rfcHandle, "unit", unit_identifier) 144 | 145 | if tlog is None: 146 | # TLog not available 147 | return RCStatus.RFC_EXTERNAL_FAILURE 148 | 149 | tid = unit_identifier["id"] 150 | tlog_record = tlog[tid] 151 | if tlog_record is None: 152 | return RCStatus.RFC_NOT_FOUND 153 | return RCStatus(tlog_record["status"]) 154 | 155 | 156 | # create server 157 | 158 | 159 | server = Server(*BACKEND[backend_dest]) 160 | print(server.get_server_attributes()) 161 | 162 | try: 163 | # expose python function stfc_write_to_tcpic as ABAP function STFC_WRITE_TO_TCPIC, 164 | # to be called by ABAP system 165 | server.add_function("STFC_WRITE_TO_TCPIC", stfc_write_to_tcpic) 166 | 167 | # register bgRFC handlers 168 | server.bgrfc_init( 169 | backend_dest, 170 | { 171 | "check": onCheckFunction, 172 | "commit": onCommitFunction, 173 | "rollback": onRollbackFunction, 174 | "confirm": onConfirmFunction, 175 | "getState": onGetStateFunction, 176 | }, 177 | ) 178 | 179 | # start server 180 | server.start() 181 | 182 | input("Press Enter key to stop server...\n") 183 | 184 | # stop server and database 185 | server.stop() 186 | 187 | finally: 188 | # clean-up the server 189 | server.close() 190 | 191 | tlog.dump() 192 | -------------------------------------------------------------------------------- /src/pyrfc/_exception.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | """:mod:`pyrfc`-specific exception classes.""" 6 | 7 | from enum import Enum, auto 8 | 9 | from pyrfc._utils import enum_values 10 | 11 | 12 | class RFCError(Exception): 13 | """Exception base class. 14 | 15 | Indicates that there was an error in the Python connector. 16 | """ 17 | 18 | 19 | class RFCLibError(RFCError): 20 | """RFC library error. 21 | 22 | Base class for exceptions raised by the local underlying C connector (sapnwrfc.c). 23 | """ 24 | 25 | def __init__( # noqa: PLR0913 PLR0917 26 | self, 27 | message=None, 28 | code=None, 29 | key=None, 30 | msg_class=None, 31 | msg_type=None, 32 | msg_number=None, 33 | msg_v1=None, 34 | msg_v2=None, 35 | msg_v3=None, 36 | msg_v4=None, 37 | ): 38 | """Init RFCLibError class.""" 39 | super(RFCLibError, self).__init__(message) 40 | self.message = message # Exception.message removed in Py3 41 | self.code = code 42 | self.key = key 43 | self.msg_class = msg_class 44 | self.msg_type = msg_type 45 | self.msg_number = msg_number 46 | self.msg_v1 = msg_v1 47 | self.msg_v2 = msg_v2 48 | self.msg_v3 = msg_v3 49 | self.msg_v4 = msg_v4 50 | 51 | def __str__(self): 52 | """Convert RFCLibError object to string.""" 53 | code = 28 if self.code is None else self.code # 28 = RFC_UNKNOWN_ERROR 54 | rc_text = RcCodeText(code).value if code in enum_values(RcCodeText) else "??" 55 | return ( 56 | f"{rc_text} (rc={self.code}): key={self.key}, message={self.message}" 57 | f" [MSG: class={self.msg_class}, type={self.msg_type}, number={self.msg_number}," # noqa: E501 58 | f" v1-4:={self.msg_v1};{self.msg_v2};{self.msg_v3};{self.msg_v4}]" 59 | ) 60 | 61 | 62 | class ABAPApplicationError(RFCLibError): 63 | """ABAP application error. 64 | 65 | This exception is raised if a RFC call returns an RC code greater than 0 66 | and the error object has an RFC_ERROR_GROUP value of 67 | ABAP_APPLICATION_FAILURE. 68 | """ 69 | 70 | 71 | class ABAPRuntimeError(RFCLibError): 72 | """ABAP runtime error. 73 | 74 | This exception is raised if a RFC call returns an RC code greater than 0 75 | and the error object has an RFC_ERROR_GROUP value of 76 | ABAP_RUNTIME_FAILURE. 77 | """ 78 | 79 | 80 | class LogonError(RFCLibError): 81 | """Logon error. 82 | 83 | This exception is raised if a RFC call returns an RC code greater than 0 84 | and the error object has an RFC_ERROR_GROUP value of 85 | LOGON_FAILURE. 86 | """ 87 | 88 | def __init__( # noqa: PLR0913 PLR0917 89 | self, 90 | message=None, 91 | code=2, 92 | key="RFC_LOGON_FAILURE", 93 | msg_class=None, 94 | msg_type=None, 95 | msg_number=None, 96 | msg_v1=None, 97 | msg_v2=None, 98 | msg_v3=None, 99 | msg_v4=None, 100 | ): 101 | """Init LogonError.""" 102 | # Setting default values allows for raising an error with one parameter. 103 | super(LogonError, self).__init__( 104 | message, 105 | code, 106 | key, 107 | msg_class, 108 | msg_type, 109 | msg_number, 110 | msg_v1, 111 | msg_v2, 112 | msg_v3, 113 | msg_v4, 114 | ) 115 | 116 | 117 | class CommunicationError(RFCLibError): 118 | """Communication error. 119 | 120 | This exception is raised if a RFC call returns an RC code greater than 0 121 | and the error object has an RFC_ERROR_GROUP value of 122 | COMMUNICATION_FAILURE. 123 | """ 124 | 125 | 126 | class ExternalRuntimeError(RFCLibError): 127 | """External runtime error. 128 | 129 | This exception is raised if a RFC call returns an RC code greater than 0 130 | and the error object has an RFC_ERROR_GROUP value of 131 | EXTERNAL_RUNTIME_FAILURE. 132 | """ 133 | 134 | 135 | class ExternalApplicationError(RFCLibError): 136 | """External application error. 137 | 138 | This exception is raised if a RFC call returns an RC code greater than 0 139 | and the error object has an RFC_ERROR_GROUP value of 140 | EXTERNAL_APPLICATION_FAILURE. 141 | """ 142 | 143 | 144 | class ExternalAuthorizationError(RFCLibError): 145 | """External authorization error. 146 | 147 | This exception is raised if a RFC call returns an RC code greater than 0 148 | and the error object has an RFC_ERROR_GROUP value of 149 | EXTERNAL_AUTHORIZATION_FAILURE. 150 | """ 151 | 152 | 153 | class RFCTypeError(RFCLibError): 154 | """Type concersion error. 155 | 156 | This exception is raised when invalid data type detected in RFC input 157 | (fill) conversion and the error object has an RFC_ERROR_GROUP value of 158 | RFC_TYPE_ERROR 159 | """ 160 | 161 | 162 | class RcCodeText(Enum): 163 | """RFC library return codes.""" 164 | 165 | RFC_OK = auto() 166 | RFC_COMMUNICATION_FAILURE = auto() 167 | RFC_LOGON_FAILURE = auto() 168 | RFC_ABAP_RUNTIME_FAILURE = auto() 169 | RFC_ABAP_MESSAGE = auto() 170 | RFC_ABAP_EXCEPTION = auto() 171 | RFC_CLOSED = auto() 172 | RFC_CANCELED = auto() 173 | RFC_TIMEOUT = auto() 174 | RFC_MEMORY_INSUFFICIENT = auto() 175 | RFC_VERSION_MISMATCH = auto() 176 | RFC_INVALID_PROTOCOL = auto() 177 | RFC_SERIALIZATION_FAILURE = auto() 178 | RFC_INVALID_HANDLE = auto() 179 | RFC_RETRY = auto() 180 | RFC_EXTERNAL_FAILURE = auto() 181 | RFC_EXECUTED = auto() 182 | RFC_NOT_FOUND = auto() 183 | RFC_NOT_SUPPORTED = auto() 184 | RFC_ILLEGAL_STATE = auto() 185 | RFC_INVALID_PARAMETER = auto() 186 | RFC_CODEPAGE_CONVERSION_FAILURE = auto() 187 | RFC_CONVERSION_FAILURE = auto() 188 | RFC_BUFFER_TOO_SMALL = auto() 189 | RFC_TABLE_MOVE_BOF = auto() 190 | RFC_TABLE_MOVE_EOF = auto() 191 | RFC_START_SAPGUI_FAILURE = auto() 192 | RFC_ABAP_CLASS_EXCEPTION = auto() 193 | RFC_UNKNOWN_ERROR = auto() 194 | RFC_AUTHORIZATION_FAILURE = auto() 195 | RFC_AUTHENTICATION_FAILURE = auto() 196 | RFC_CRYPTOLIB_FAILURE = auto() 197 | RFC_IO_FAILURE = auto() 198 | RFC_LOCKING_FAILURE = auto() 199 | -------------------------------------------------------------------------------- /doc/pyrfc.rst: -------------------------------------------------------------------------------- 1 | .. To achieve a printout of the class docstring (for the __init__ method), 2 | but also information about the methods _with signatures_, we have to use 3 | .. autoclass: bla (for the class docstring, without auto there is no docstring) 4 | .. automethod: foo (for the method definitions with (manual) signatures. 5 | 6 | .. cf. http://stackoverflow.com/questions/11830242/non-breaking-space 7 | 8 | .. |nbsp| unicode:: 0xA0 9 | :trim: 10 | 11 | .. currentmodule:: pyrfc 12 | 13 | ################### 14 | :mod:`pyrfc` 15 | ################### 16 | 17 | The :mod:`pyrfc` package. 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | 22 | .. _pyrfcapi: 23 | 24 | ====================== 25 | PyRFC module functions 26 | ====================== 27 | 28 | .. autofunction:: get_nwrfclib_version 29 | .. autofunction:: set_ini_file_directory(path_name) 30 | .. autofunction:: reload_ini_file 31 | .. autofunction:: language_iso_to_sap(lang_iso) 32 | .. autofunction:: language_sap_to_iso(lang_sap) 33 | .. autofunction:: set_cryptolib_path(path_name) 34 | .. autofunction:: set_locale_radix(value=None) 35 | .. autofunction:: cancel_connection(client_connection) 36 | 37 | .. _apiconn: 38 | 39 | ========== 40 | Connection 41 | ========== 42 | 43 | .. autoclass:: Connection 44 | 45 | .. autoattribute:: alive 46 | .. autoattribute:: handle 47 | .. autoattribute:: options 48 | .. automethod:: get_connection_attributes() 49 | .. automethod:: open() 50 | .. automethod:: reopen() 51 | .. automethod:: call(func_name, options, params) 52 | .. automethod:: close() 53 | .. automethod:: cancel() 54 | .. automethod:: free() 55 | .. automethod:: ping() 56 | .. automethod:: reset_server_context() 57 | .. automethod:: is_valid() 58 | .. automethod:: initialize_unit([background=True]) 59 | .. automethod:: fill_and_submit_unit(unit, calls[, queue_names=None[, attributes=None]]) 60 | .. automethod:: confirm_unit(unit) 61 | .. automethod:: destroy_unit(unit) 62 | .. automethod:: get_unit_state(unit) 63 | .. automethod:: get_function_description(func_name) 64 | .. automethod:: type_desc_get(type_name) 65 | .. automethod:: type_desc_remove(type_name) 66 | .. automethod:: func_desc_remove(func_name) 67 | 68 | .. _apiserver: 69 | 70 | ========== 71 | Server 72 | ========== 73 | 74 | .. autoclass:: Server 75 | 76 | .. automethod:: bgrfc_init(sysId, bgRfcFunction) 77 | .. automethod:: add_function(func_name, callback) 78 | .. automethod:: serve() 79 | .. automethod:: start() 80 | .. automethod:: stop() 81 | .. automethod:: close() 82 | .. automethod:: get_server_attributes() 83 | 84 | .. _apiconnectionparams: 85 | 86 | ===================== 87 | Connection Parameters 88 | ===================== 89 | 90 | .. autoclass:: ConnectionParameters 91 | 92 | .. _apifuncdesc: 93 | 94 | ==================== 95 | Function Description 96 | ==================== 97 | 98 | .. note:: 99 | 100 | Actually, the FunctionDescription class does not support exceptions. 101 | 102 | .. autoclass:: FunctionDescription 103 | 104 | .. automethod:: add_parameter(name, parameter_type, direction, nuc_length, uc_length[, decimals=0[, default_value=""[, parameter_text=""[, optional=False[, type_description=None]]]]]) 105 | 106 | .. _apitypedesc: 107 | 108 | =================== 109 | Type Description 110 | =================== 111 | .. autoclass:: TypeDescription 112 | 113 | .. automethod:: add_field(name, field_type, nuc_length, uc_length, nuc_offset, uc_offset[, decimals=0[, type_description=None]]) 114 | 115 | .. _apithrououghput: 116 | 117 | ========== 118 | Throughput 119 | ========== 120 | 121 | .. autoclass:: Throughput 122 | 123 | .. autoattribute:: _handle 124 | .. autoattribute:: connections 125 | .. autoattribute:: stats 126 | .. automethod:: setOnConnection() 127 | .. automethod:: getFromConnection() 128 | .. automethod:: removeFromConnection() 129 | .. automethod:: reset() 130 | 131 | .. _apierr: 132 | 133 | ====== 134 | Errors 135 | ====== 136 | 137 | If a problem occurs in the Python connector or in an underlying component (e.g. 138 | C connector, SAP system, ABAP code, ...), an exception is raised. The class 139 | of the exception indicates where the problem occurred. 140 | 141 | 1. ``RFCError``: This error is raised, if a problem occurred in the Python 142 | connector. 143 | 2. ``RFCLibError``: This error is raised, if a problem occurred in the C 144 | connector. 145 | 3. All other errors represent errors with the RFC call to the SAP backend 146 | system. For these errors, the errorInfo struct of the C connector is wrapped, 147 | e.g. for a given exception ``e``, the error code is available in ``e.code``. 148 | The class of the error depends on the group of the error. 149 | 150 | .. image:: _static/images/exceptions.* 151 | :alt: Inheritance of errors: Exception->RFCError->RFCLibError->specific errors 152 | :align: center 153 | :scale: 90% 154 | :name: Inheritance of errors 155 | 156 | .. autoexception:: RFCError 157 | 158 | .. autoexception:: RFCLibError 159 | 160 | .. autoexception:: LogonError 161 | 162 | .. autoexception:: CommunicationError 163 | 164 | .. autoexception:: ABAPApplicationError 165 | 166 | .. autoexception:: ABAPRuntimeError 167 | 168 | .. autoexception:: ExternalAuthorizationError 169 | 170 | .. autoexception:: ExternalRuntimeError 171 | 172 | .. autoexception:: ExternalApplicationError 173 | 174 | 175 | .. _error-types: 176 | 177 | Error types, codes, groups, and classes 178 | ======================================= 179 | 180 | :ref:`Schmidt and Li (2009a)` describe four possible *error types* on 181 | the basis of the return code (i.e. *error code*) of a RFM invocation: 182 | 183 | * ABAP exception, 184 | * system failure, 185 | * ABAP messages, and 186 | * communication failure. 187 | 188 | However, there are in total roughly 30 possible return codes that indicate some 189 | kind of error. As each error information struct provides an 190 | *error group* information with seven possible groups, 191 | which was taken as the basis for the exception *classes*. 192 | 193 | The following table should facilitate the matching between the different 194 | error representations. 195 | 196 | ======================= =============================== =========================== ==================== 197 | type (SPJ) code [numeric] (C) group (C) class (Python) 198 | ======================= =============================== =========================== ==================== 199 | ABAP exception RFC_ABAP_EXCEPTION [5] ABAP_APPLICATION_FAILURE ABAPApplicationError 200 | system failure RFC_ABAP_RUNTIME_FAILURE [3] ABAP_RUNTIME_FAILURE ABAPRuntimeError 201 | ABAP message RFC_ABAP_MESSAGE [4] ABAP_RUNTIME_FAILURE ABAPRuntimeError 202 | communication failure RFC_COMMUNICATION_FAILURE [1] COMMUNICATION_FAILURE CommunicationError 203 | \ RFC_LOGON_FAILURE [2] LOGON_FAILURE LogonError 204 | ======================= =============================== =========================== ==================== 205 | -------------------------------------------------------------------------------- /tests/config.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import datetime 6 | import platform 7 | from configparser import ConfigParser 8 | 9 | from pyrfc import set_ini_file_directory 10 | 11 | COPA = ConfigParser() 12 | with open("tests/pyrfc.cfg", "r") as fc: 13 | COPA.read_file(fc) 14 | 15 | # Numeric types 16 | # 17 | # ABAP: https://help.sap.com/doc/abapdocu_752_index_htm/7.52/en-US/index.htm?file=abenddic_builtin_types_intro.htm 18 | # JavaScript: https://www.ecma-international.org/ecma-262/10.0/index.html#Title 19 | # 20 | 21 | latest_python_version = ( 22 | 3, 23 | 12, 24 | ) 25 | 26 | # Numeric types 27 | # 28 | # ABAP: https://help.sap.com/doc/abapdocu_752_index_htm/7.52/en-US/index.htm?file=abenddic_builtin_types_intro.htm 29 | # JavaScript: https://www.ecma-international.org/ecma-262/10.0/index.html#Title 30 | # 31 | 32 | RFC_MATH = { 33 | "RFC_INT1": {"MIN": 0, "MAX": 255}, 34 | "RFC_INT2": {"NEG": -32768, "POS": 32767}, 35 | "RFC_INT4": {"NEG": -2147483648, "POS": 2147483647}, 36 | "RFC_INT8": {"NEG": -9223372036854775808, "POS": 9223372036854775807}, 37 | "FLOAT": { 38 | "NEG": {"MIN": "-2.2250738585072014E-308", "MAX": "-1.7976931348623157E+308"}, 39 | "POS": {"MIN": "2.2250738585072014E-308", "MAX": "1.7976931348623157E+308"}, 40 | }, 41 | "DECF16": { 42 | "NEG": {"MIN": "-1E-383", "MAX": "-9.999999999999999E+384"}, 43 | "POS": {"MIN": "1E-383", "MAX": "9.999999999999999E+384"}, 44 | }, 45 | "DECF34": { 46 | "NEG": {"MIN": "-1E-6143", "MAX": "-9.999999999999999999999999999999999E+6144"}, 47 | "POS": {"MIN": "1E-6143", "MAX": "9.999999999999999999999999999999999E+6144"}, 48 | }, 49 | "DATE": {"MIN": "00010101", "MAX": "99991231"}, 50 | "TIME": {"MIN": "000000", "MAX": "235959"}, 51 | "UTCLONG": { 52 | "MIN": "0001-01-01T00:00:00.0000000", 53 | "MAX": "9999-12-31T23:59:59.9999999", 54 | "INITIAL": "0000-00-00T00:00:00.0000000", 55 | }, 56 | } 57 | 58 | 59 | def ABAP_to_python_date(abap_date): 60 | return datetime.datetime.strptime(abap_date, "%Y%m%d").date() 61 | 62 | 63 | def ABAP_to_python_time(abap_time): 64 | return datetime.datetime.strptime(abap_time, "%H%M%S").time() 65 | 66 | 67 | def python_to_ABAP_date(py_date): 68 | return f"{py_date.year:04d}{py_date.month:02d}{py_date.day:02d}" 69 | 70 | 71 | def python_to_ABAP_time(py_time): 72 | return f"{py_time.hour:02d}{py_time.minute:02d}{py_time.second:02d}" 73 | 74 | 75 | set_ini_file_directory("tests") 76 | 77 | CONNECTION_PARAMS = dict(COPA["coevi51"]) 78 | CONNECTION_DEST = dict(COPA["coevi51dest"]) 79 | UNICODETEST = "ทดสอบสร้างลูกค้าจากภายนอกครั้งที่" * 7 80 | UNICODE1 = "四周远处都" 81 | BYTEARRAY_TEST = bytearray.fromhex("01414243444549500051fdfeff") 82 | BYTES_TEST = bytes(BYTEARRAY_TEST) 83 | 84 | PLATFORM = platform.system().lower() 85 | CryptoLibPath = { 86 | "darwin": "/Applications/Secure Login Client.app/Contents/MacOS/lib/libsapcrypto.dylib", # noqa: E501 87 | "linux": "/usr/local/sap/cryptolib/libsapcrypto.so", 88 | "win32": "C:\\Tools\\cryptolib\\sapcrypto.dll", 89 | "windows": "C:\\Tools\\cryptolib\\sapcrypto.dll", 90 | # "C:\\Program Files\\SAP\\FrontEnd\\SecureLogin\\libsapcrypto.dll" 91 | }[PLATFORM] 92 | 93 | ClientPSEPath = { 94 | "darwin": "/Users/d037732/dotfiles/sec/rfctest.pse", 95 | "linux": "/home/www-admin/sec/rfctest.pse", 96 | "win32": "C:\\Tools\\sec\\rfctest.pse", 97 | "windows": "C:\\Tools\\sec\\rfctest.pse", 98 | }[PLATFORM] 99 | 100 | LANGUAGES = { 101 | "AF": {"lang_sap": "a", "text": "Afrikaans"}, 102 | "SQ": {"lang_sap": "뽑", "text": "Albanian"}, 103 | "AG": {"lang_sap": "뢇", "text": "Algonquian"}, 104 | "AR": {"lang_sap": "A", "text": "Arabic"}, 105 | "AZ": {"lang_sap": "뢚", "text": "Azerbaijani"}, 106 | "BD": {"lang_sap": "룤", "text": "Banda"}, 107 | "BB": {"lang_sap": "룢", "text": "Bemba"}, 108 | "BN": {"lang_sap": "룮", "text": "Bengali"}, 109 | "BK": {"lang_sap": "룫", "text": "Bikol"}, 110 | "BS": {"lang_sap": "룳", "text": "Bosnian"}, 111 | "Z9": {"lang_sap": "&", "text": "Brazilian Prtugu"}, 112 | "BG": {"lang_sap": "W", "text": "Bulgarian"}, 113 | "CA": {"lang_sap": "c", "text": "Catalan"}, 114 | "ZH": {"lang_sap": "1", "text": "Chinese"}, 115 | "ZF": {"lang_sap": "M", "text": "Chinese trad."}, 116 | "KW": {"lang_sap": "뱗", "text": "Cornish"}, 117 | "HR": {"lang_sap": "6", "text": "Croatian"}, 118 | "Z1": {"lang_sap": "Z", "text": "Customer reserve"}, 119 | "CS": {"lang_sap": "C", "text": "Czech"}, 120 | "DA": {"lang_sap": "K", "text": "Danish"}, 121 | "NL": {"lang_sap": "N", "text": "Dutch"}, 122 | "DM": {"lang_sap": "릭", "text": "Dutch, Middle"}, 123 | "EN": {"lang_sap": "E", "text": "English"}, 124 | "6N": {"lang_sap": "둮", "text": "English GB"}, 125 | "ET": {"lang_sap": "9", "text": "Estonian"}, 126 | "FI": {"lang_sap": "U", "text": "Finnish"}, 127 | "FR": {"lang_sap": "F", "text": "French"}, 128 | "3F": {"lang_sap": "덆", "text": "French CA"}, 129 | "DE": {"lang_sap": "D", "text": "German"}, 130 | "4G": {"lang_sap": "뎧", "text": "German_CH"}, 131 | "EL": {"lang_sap": "G", "text": "Greek"}, 132 | "HE": {"lang_sap": "B", "text": "Hebrew"}, 133 | "HI": {"lang_sap": "묩", "text": "Hindi"}, 134 | "HU": {"lang_sap": "H", "text": "Hungarian"}, 135 | "IS": {"lang_sap": "b", "text": "Icelandic"}, 136 | "IN": {"lang_sap": "뮎", "text": "Indic"}, 137 | "ID": {"lang_sap": "i", "text": "Indonesian"}, 138 | "IR": {"lang_sap": "뮒", "text": "Iranian"}, 139 | "IT": {"lang_sap": "I", "text": "Italian"}, 140 | "JA": {"lang_sap": "J", "text": "Japanese"}, 141 | "KK": {"lang_sap": "뱋", "text": "Kazakh"}, 142 | "KO": {"lang_sap": "3", "text": "Korean"}, 143 | "LV": {"lang_sap": "Y", "text": "Latvian"}, 144 | "LT": {"lang_sap": "X", "text": "Lithuanian"}, 145 | "MK": {"lang_sap": "봋", "text": "Macedonian"}, 146 | "MS": {"lang_sap": "7", "text": "Malay"}, 147 | "MV": {"lang_sap": "봖", "text": "Manipuri"}, 148 | "MO": {"lang_sap": "봏", "text": "Moldavian"}, 149 | "NI": {"lang_sap": "뵩", "text": "Niger-Kordofa"}, 150 | "NO": {"lang_sap": "O", "text": "Norwegian"}, 151 | "OM": {"lang_sap": "뷍", "text": "Oromo"}, 152 | "P1": {"lang_sap": "븑", "text": "Phillipine"}, 153 | "PL": {"lang_sap": "L", "text": "Polish"}, 154 | "PT": {"lang_sap": "P", "text": "Portuguese"}, 155 | "1P": {"lang_sap": "느", "text": "Portuguese PT"}, 156 | "PK": {"lang_sap": "븫", "text": "Prakrit"}, 157 | "RO": {"lang_sap": "4", "text": "Romanian"}, 158 | "RU": {"lang_sap": "R", "text": "Russian"}, 159 | "SA": {"lang_sap": "뽁", "text": "Sanskrit"}, 160 | "SR": {"lang_sap": "0", "text": "Serbian"}, 161 | "SH": {"lang_sap": "d", "text": "Serbian (Latin)"}, 162 | "SK": {"lang_sap": "Q", "text": "Slovak"}, 163 | "SL": {"lang_sap": "5", "text": "Slovenian"}, 164 | "ES": {"lang_sap": "S", "text": "Spanish"}, 165 | "1X": {"lang_sap": "늘", "text": "Spanish MX"}, 166 | "SV": {"lang_sap": "V", "text": "Swedish"}, 167 | "TA": {"lang_sap": "뾡", "text": "Tamil"}, 168 | "TT": {"lang_sap": "뾴", "text": "Tatar"}, 169 | "1Q": {"lang_sap": "늑", "text": "Technical code 1"}, 170 | "2Q": {"lang_sap": "닱", "text": "Technical code 2"}, 171 | "TH": {"lang_sap": "2", "text": "Thai"}, 172 | "TR": {"lang_sap": "T", "text": "Turkish"}, 173 | "TC": {"lang_sap": "뾣", "text": "Tuvinain"}, 174 | "Z8": {"lang_sap": ";", "text": "US English"}, 175 | "UK": {"lang_sap": "8", "text": "Ukrainian"}, 176 | "VI": {"lang_sap": "쁩", "text": "Vietnamese"}, 177 | } 178 | -------------------------------------------------------------------------------- /doc/install.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | ============ 4 | Installation 5 | ============ 6 | 7 | If `SAP NetWeaver RFC SDK `_ and Python 8 | are already installed on your system, you can pip install the :mod:`pyrfc` wheel from the `latest release `_, 9 | or clone this repository and build :mod:`pyrfc` from the source code, following :ref:`build`. 10 | 11 | Use the Python 3 and the latest pyrfc and SAP NWRFC SDK release (fully backwards compatible). 12 | 13 | .. _install-c-connector: 14 | 15 | SAP NWRFC SDK Installation 16 | =========================== 17 | 18 | If SAP NWRFC SDK is already installed on your system, you may verify the installation by running the ``rfcexec`` utility, without any parameter. 19 | 20 | The error message like below indicates that SAP NWRFC SDK installation is technically correct, expecting more input parameters. 21 | Different error message may be caused by missing Windows C++ binary for example, or another installation inconsistency: 22 | 23 | .. code-block:: sh 24 | 25 | $ cd /usr/local/sap/nwrfcsdk/bin 26 | $ ./startrfc -v 27 | NW RFC Library Version: 750 Patch Level 12 28 | Compiler Version: 29 | Version not available. 30 | Startrfc Version: 2018-08-15 31 | 32 | 33 | Information on where to download the SAP NWRFC SDK you may find `here `_ . 34 | 35 | The PyRFC connector relies on *SAP NWRFC SDK* and must be able to find the library 36 | files at runtime. Therefore, you might either install the *SAP NWRFC SDK* 37 | in the standard library paths of your system or install it in any location and tell the 38 | Python connector where to look. 39 | 40 | Here are configuration examples for Windows, Linux and macOS operating systems. 41 | 42 | Windows 43 | ------- 44 | 45 | 1. Create the SAP NWRFC SDK home directory, e.g. ``c:\nwrfcsdk`` 46 | 2. Set the SAPNWRFC_HOME env variable: ``SAPNWRFC_HOME=c:\nwrfcsdk`` 47 | 3. Unpack the SAP NWRFC SDK archive to it, e.g. ``c:\nwrfcsdk\lib`` shall exist. 48 | 4. Include the ``lib`` directory to the library search path on Windows, i.e. 49 | :ref:`extend` the ``PATH`` environment variable. 50 | 51 | Add ``c:\nwrfcsdk\lib`` to PATH. 52 | 53 | Linux 54 | ----- 55 | 56 | 1. Create the SAP NWRFC SDK home directory, e.g. ``/usr/local/sap/nwrfcsdk`` 57 | 2. Set the SAPNWRFC_HOME env variable: ``SAPNWRFC_HOME=/usr/local/sap/nwrfcsdk`` 58 | 3. Unpack the SAP NWRFC SDK archive to it, e.g. ``/usr/local/sap/nwrfcsdk/lib`` shall exist. 59 | 4. Include the ``lib`` directory in the library search path: 60 | 61 | * As ``root``, create a file ``/etc/ld.so.conf.d/nwrfcsdk.conf`` and 62 | enter the following values: 63 | 64 | .. code-block:: sh 65 | 66 | # include nwrfcsdk 67 | /usr/local/sap/nwrfcsdk/lib 68 | 69 | * As ``root``, run the command ``ldconfig``. To check if libraries are installed: 70 | 71 | .. code-block:: sh 72 | 73 | $ ldconfig -p | grep sap # should show something like: 74 | libsapucum.so (libc6,x86-64) => /usr/local/sap/nwrfcsdk/lib/libsapucum.so 75 | libsapnwrfc.so (libc6,x86-64) => /usr/local/sap/nwrfcsdk/lib/libsapnwrfc.so 76 | libicuuc.so.50 (libc6,x86-64) => /usr/local/sap/nwrfcsdk/lib/libicuuc.so.50 77 | libicui18n.so.50 (libc6,x86-64) => /usr/local/sap/nwrfcsdk/lib/libicui18n.so.50 78 | libicudecnumber.so (libc6,x86-64) => /usr/local/sap/nwrfcsdk/lib/libicudecnumber.so 79 | libicudata.so.50 (libc6,x86-64) => /usr/local/sap/nwrfcsdk/lib/libicudata.so.50 80 | libgssapi_krb5.so.2 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2 81 | libgssapi.so.3 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libgssapi.so.3 82 | $ 83 | 84 | macOS 85 | ----- 86 | 87 | 1. Create the SAP NWRFC SDK root directory ``/usr/local/sap/nwrfcsdk`` (this location is fixed, more info below) 88 | 2. Set SAPNWRFC_HOME environment variable to that location: ``SAPNWRFC_HOME=/usr/local/sap/nwrfcsdk`` 89 | 3. Unpack the SAP NWRFC SDK archive to it, e.g. ``/usr/local/sap/nwrfcsdk/lib`` shall exist. 90 | 4. Set the remote paths in SAP NWRFC SDK by running `paths_fix.sh `_ script. 91 | 92 | This location is fixed to the default ``/usr/local/sap/nwrfcsdk/lib`` rpath, embedded into node-rfc package published on npm. 93 | 94 | After moving SAP NWRFC SDK to another location on your system, the rpaths must be adjusted in SAP NWRFC SDK and in pyrfc.so libraries. 95 | 96 | For SAP NWRFC SDK, set the SAPNWRFC_HOME env variable to new SAP NWRFC SDK root directory and re-run the above script. 97 | 98 | For pyrfc: 99 | 100 | .. code-block:: sh 101 | 102 | $ unzip unzip pyrfc-2.0.1-cp38-cp38-macosx_10_15_x86_64.whl 103 | $ cd pyrfc 104 | $ install_name_tool -rpath /usr/local/sap/nwrfcsdk/lib _pyrfc.cpython-38-darwin.so 105 | 106 | 107 | .. _install-python-connector: 108 | 109 | Python Connector Installation 110 | ============================= 111 | 112 | see `README: Download and Installation `_ 113 | 114 | Run ``python`` and type ``from pyrfc import *``. If this finishes silently, without oputput, the installation was successful. 115 | 116 | Using virtual environments you can isolate Python/PyRFC projects, working without administrator privileges. 117 | 118 | macOS 119 | ----- 120 | 121 | .. _`install-python-macOS`: 122 | 123 | The macOS system version of Python is usually the older one. Using wirtual environments, 124 | like `pyenv `_ for example, is recommended: 125 | 126 | .. code-block:: sh 127 | 128 | pyenv install 3.8.0 129 | pyenv virtualenv 3.8.0 py380 130 | 131 | Install the Python connector the same way like for Linux. 132 | 133 | .. _install-problems: 134 | 135 | Problems 136 | ======== 137 | 138 | Behind a Proxy 139 | -------------- 140 | 141 | If you are within an internal network that accesses the internet through 142 | an HTTP(S) proxy, some of the shell commands will fail with urlopen errors, etc. 143 | 144 | Assuming that your HTTP(S) proxy could be accessed via ``http://proxy:8080``, on Windows 145 | you can communicate this proxy to your shell via:: 146 | 147 | SET HTTP_PROXY=http://proxy:8080 148 | SET HTTPS_PROXY=http://proxy:8080 149 | 150 | or permanently set environment variables. 151 | 152 | 153 | SAP NW RFC Library Installation 154 | ------------------------------- 155 | 156 | 1. ``ImportError: DLL load failed: The specified module could not be found.`` 157 | 158 | (Windows) 159 | This error indicates that the Python connector was not able to find the 160 | C connector on your system. Please check, if the ``lib`` directory of the 161 | C connector is in your ``PATH`` environment variable. 162 | 163 | 2. ``ImportError: DLL load failed: %1 is not a valid Win32 application.`` 164 | 165 | (Windows) 166 | This error occurs when SAP NW RFC Library 64bit version is installed on a system with 32bit version Python. 167 | 168 | Environment variables 169 | --------------------- 170 | 171 | .. _install-problems-envvar-win: 172 | 173 | Windows 174 | ''''''' 175 | The environment variable may be set within a command prompt via the ``set`` 176 | command, e.g. 177 | 178 | * ``set PATH=%PATH%;C:\nwrfcsdk\lib`` (extend PATH with the C connector lib) 179 | * ``set HTTPS_PROXY=proxy:8080`` (setting an proxy for HTTPS communication) 180 | 181 | When the command prompt is closed, the environment variable is reset. To achieve 182 | a persistent change of the environment variable, do the following (Windows 7): 183 | 184 | 1. Open the Start Menu and type ``environment`` into the search box. 185 | 2. A window opens in which the user variables are displayed in the upper part 186 | and the system variables in the lower part. You may select and edit 187 | the desired variable. 188 | 3. The modified variables are used when a *new* command prompt is opened. 189 | -------------------------------------------------------------------------------- /tests/test_errors_abap.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import pytest 6 | from pyrfc import ABAPApplicationError, ABAPRuntimeError, Connection, RFCError 7 | 8 | from tests.config import CONNECTION_PARAMS as params 9 | 10 | 11 | class TestErrorsABAP: 12 | def setup_method(self): 13 | self.conn = Connection(**params) 14 | assert self.conn.alive 15 | 16 | def teardown_method(self): 17 | self.conn.close() 18 | assert not self.conn.alive 19 | 20 | def test_no_connection_params(self): 21 | with pytest.raises(RFCError) as ex: 22 | Connection() 23 | error = ex.value 24 | assert error.args[0] == "Connection parameters missing" 25 | 26 | def test_RFC_RAISE_ERROR(self): 27 | with pytest.raises(ABAPRuntimeError) as ex: 28 | self.conn.call( 29 | "RFC_RAISE_ERROR", 30 | MESSAGETYPE="A", 31 | ) 32 | assert self.conn.alive is True 33 | error = ex.value 34 | assert error.code == 4 35 | assert error.key == "Function not supported" 36 | assert error.message == "Function not supported" 37 | assert error.msg_class == "SR" 38 | assert error.msg_type == "A" 39 | assert error.msg_number == "006" 40 | 41 | def test_STFC_SAPGUI(self): 42 | # STFC_SAPGUI RFC-TEST: RFC with SAPGUI 43 | with pytest.raises(ABAPRuntimeError) as ex: 44 | self.conn.call("STFC_SAPGUI") 45 | assert self.conn.alive is True 46 | error = ex.value 47 | assert error.code == 3 48 | assert error.key == "DYNPRO_SEND_IN_BACKGROUND" 49 | 50 | def test_AbapRuntimeError_E0(self): 51 | # RFC_RAISE_ERROR ARFC: Raise Different Type of Error Message 52 | # Comment: cf. result_print of the error_test.py 53 | # cf. ExceptionTest.c (l. 92ff) 54 | with pytest.raises(ABAPRuntimeError) as ex: 55 | self.conn.call( 56 | "RFC_RAISE_ERROR", 57 | METHOD="0", 58 | MESSAGETYPE="E", 59 | ) 60 | error = ex.value 61 | assert self.conn.alive is True 62 | assert error.code == 4 63 | assert error.message == "Function not supported" 64 | 65 | def test_AbapApplicationError_E1(self): 66 | # Comment: cf. result_print of the error_test.py 67 | # '1_E': 'ABAPApplicationError-5-RAISE_EXCEPTION-ID:SR Type:E Number:006 STRING-True' # noqa: E501 68 | # cf. ExceptionTest.c (l. 75ff) 69 | with pytest.raises(ABAPApplicationError) as ex: 70 | self.conn.call( 71 | "RFC_RAISE_ERROR", 72 | METHOD="1", 73 | MESSAGETYPE="E", 74 | ) 75 | assert self.conn.alive is True 76 | error = ex.value 77 | assert error.code == 5 78 | assert error.key == "RAISE_EXCEPTION" 79 | assert error.msg_class == "SR" 80 | assert error.msg_type == "E" 81 | assert error.msg_number == "006" 82 | 83 | def test_AbapApplicationError_E2(self): 84 | # '2_E': 'ABAPApplicationError-5-RAISE_EXCEPTION- Number:000-True', 85 | # cf. ExceptionTest.c (l. 65ff) 86 | with pytest.raises(ABAPApplicationError) as ex: 87 | self.conn.call( 88 | "RFC_RAISE_ERROR", 89 | METHOD="2", 90 | MESSAGETYPE="E", 91 | ) 92 | assert self.conn.alive is True 93 | error = ex.value 94 | assert error.code == 5 95 | assert error.key == "RAISE_EXCEPTION" 96 | assert error.msg_number == "000" 97 | 98 | def test_AbapRuntimeError_E3(self): 99 | # '3_E': 'ABAPRuntimeError-3-COMPUTE_INT_ZERODIVIDE-Division by 0 (type I)-True''] # noqa: E501 100 | # cf. ExceptionTest.c (l. 164ff) 101 | with pytest.raises(ABAPRuntimeError) as ex: 102 | self.conn.call( 103 | "RFC_RAISE_ERROR", 104 | METHOD="3", 105 | MESSAGETYPE="E", 106 | ) 107 | assert self.conn.alive is True 108 | error = ex.value 109 | assert error.code == 3 110 | assert error.key == "COMPUTE_INT_ZERODIVIDE" 111 | 112 | def test_AbapRuntimeError_A(self): 113 | # cf. ExceptionTest.c (l. 112ff) 114 | with pytest.raises(ABAPRuntimeError) as ex: 115 | self.conn.call( 116 | "RFC_RAISE_ERROR", 117 | MESSAGETYPE="A", 118 | ) 119 | assert self.conn.alive is True 120 | error = ex.value 121 | assert error.code == 4 122 | assert error.msg_class == "SR" 123 | assert error.msg_type == "A" 124 | assert error.msg_number == "006" 125 | # assert error.msg_v1 == "STRING" 126 | assert error.message == "Function not supported" 127 | 128 | def test_AbapRuntimeError_X(self): 129 | # cf. ExceptionTest.c (l. 137ff) 130 | with pytest.raises(ABAPRuntimeError) as ex: 131 | self.conn.call( 132 | "RFC_RAISE_ERROR", 133 | MESSAGETYPE="X", 134 | ) 135 | assert self.conn.alive is True 136 | error = ex.value 137 | assert error.code == 4 138 | assert error.key == "MESSAGE_TYPE_X" 139 | assert error.msg_class == "SR" 140 | assert error.msg_type == "X" 141 | assert error.msg_number == "006" 142 | # assert error.msg_v1 == "STRING" 143 | assert ( 144 | error.message 145 | == "The current application has triggered a termination with a short dump." 146 | ) 147 | 148 | def test_AbapRuntimeError_E36(self): 149 | # '36_E': 'ABAPRuntimeError-4-Division by 0 (type I)-Division by 0 (type I)-True''] # noqa: E501 150 | with pytest.raises(ABAPRuntimeError) as ex: 151 | self.conn.call( 152 | "RFC_RAISE_ERROR", 153 | METHOD="36", 154 | MESSAGETYPE="E", 155 | ) 156 | assert self.conn.alive is True 157 | error = ex.value 158 | assert error.code == 4 159 | assert "Division by 0" in str(error.message) 160 | 161 | def test_AbapRuntimeError_E51(self): 162 | # '51_E': 'ABAPRuntimeError-3-BLOCKED_COMMIT-A database commit was blocked by the application # noqa: E501 163 | with pytest.raises(ABAPRuntimeError) as ex: 164 | self.conn.call( 165 | "RFC_RAISE_ERROR", 166 | METHOD="51", 167 | MESSAGETYPE="E", 168 | ) 169 | assert self.conn.alive is True 170 | error = ex.value 171 | assert error.code == 3 172 | assert error.key == "BLOCKED_COMMIT" 173 | 174 | def test_pyrfc_exc_string(self): 175 | with pytest.raises(ABAPApplicationError) as ex: 176 | self.conn.call( 177 | "RFC_READ_TABLE", 178 | QUERY_TABLE="T008X", 179 | DELIMITER=".", 180 | ) 181 | error = ex.value 182 | assert error.code == 5 183 | assert error.key == "TABLE_NOT_AVAILABLE" 184 | assert error.message == "ID:SV Type:E Number:029 T008X" 185 | assert error.msg_type == "E" 186 | assert error.msg_number == "029" 187 | assert error.msg_v1 == "T008X" 188 | assert ( 189 | str(error) == "5 (rc=5): key=TABLE_NOT_AVAILABLE, message=ID:SV " 190 | "Type:E Number:029 T008X [MSG: class=SV, type=E, number=029, v1-4:=T008X;;;]" # noqa: E501 191 | ) 192 | 193 | # def test_ExternalRuntimeError(self): 194 | # # Comment: cf. result_print of the error_test.py 195 | # # '11_E': 'ExternalRuntimeError-17-RFC_NOT_FOUND-Function RFCPING not found-True', # noqa: E501 196 | # try: 197 | # self.conn.call("RFC_RAISE_ERROR", METHOD="17", MESSAGETYPE="E") 198 | # with pytest.raises(ExternalRuntimeError) as ex: 199 | # assert True 200 | # error = ex.value 201 | # assert error.code == 17 202 | # assert error.key =="RFC_NOT_FOUND" 203 | 204 | # def test_CommunicationError(self): 205 | # # Comment: cf. result_print of the error_test.py 206 | # # '32_E': 'CommunicationError-1-RFC_COMMUNICATION_FAILURE-connection closed without message # noqa: E501 207 | # # (CM_NO_DATA_RECEIVED)-True', 208 | # try: 209 | # self.conn.call("RFC_RAISE_ERROR", METHOD="32", MESSAGETYPE="E") 210 | 211 | # with pytest.raises(CommunicationError) as ex: 212 | # error = ex.value 213 | # assert error.code == 1 214 | # assert error.key =="RFC_COMMUNICATION_FAILURE" 215 | -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import os 6 | import sys 7 | from time import sleep 8 | 9 | import pytest 10 | from pyrfc import ( 11 | ABAPApplicationError, 12 | Connection, 13 | RCStatus, 14 | RFCError, 15 | Server, 16 | set_ini_file_directory, 17 | ) 18 | 19 | sys.path.append(os.path.dirname(__file__)) 20 | from data.func_desc_BAPISDORDER_GETDETAILEDLIST import ( 21 | FUNC_DESC_BAPISDORDER_GETDETAILEDLIST, 22 | ) 23 | from data.func_desc_BS01_SALESORDER_GETDETAIL import ( 24 | FUNC_DESC_BS01_SALESORDER_GETDETAIL, 25 | ) 26 | from data.func_desc_STFC_STRUCTURE import FUNC_DESC_STFC_STRUCTURE 27 | from function_description_utils import compare_function_description 28 | 29 | 30 | def my_stfc_connection(request_context=None, REQUTEXT=""): 31 | print("stfc invoked") 32 | print( 33 | "request_context", 34 | request_context, 35 | ) 36 | print(f"REQUTEXT: {REQUTEXT}") 37 | 38 | return { 39 | "ECHOTEXT": REQUTEXT, 40 | "RESPTEXT": "Python server here", 41 | } 42 | 43 | 44 | dir_path = os.path.dirname(os.path.realpath(__file__)) 45 | set_ini_file_directory(dir_path) 46 | 47 | server = Server( 48 | server_params={"dest": "gateway"}, 49 | client_params={"dest": "MME"}, 50 | ) 51 | 52 | client = Connection(dest="MME") 53 | 54 | 55 | @pytest.mark.skipif( 56 | not sys.platform.startswith("darwin"), reason="Manual server test on Darwin only" 57 | ) 58 | class TestServer: 59 | def test_add_wrong_function(self): 60 | with pytest.raises(ABAPApplicationError) as ex: 61 | server.add_function("STFC_CONNECTION1", my_stfc_connection) 62 | error = ex.value 63 | assert error.code == 5 64 | assert error.key == "FU_NOT_FOUND" 65 | assert error.message == "ID:FL Type:E Number:046 STFC_CONNECTION1" 66 | 67 | def test_add_function_twice(self): 68 | with pytest.raises(RFCError) as ex: 69 | server.add_function("STFC_CONNECTION", my_stfc_connection) 70 | server.add_function("STFC_CONNECTION", my_stfc_connection) 71 | error = ex.value 72 | assert error.args[0] == "Server function 'STFC_CONNECTION' already installed." 73 | 74 | def test_function_description_STFC_STRUCTURE(self): 75 | func_name = "STFC_STRUCTURE" 76 | func_desc = client.get_function_description(func_name) 77 | compare_function_description( 78 | func_desc, 79 | FUNC_DESC_STFC_STRUCTURE, 80 | ) 81 | 82 | def test_function_description_BAPISDORDER_GETDETAILEDLIST(self): 83 | func_name = "BAPISDORDER_GETDETAILEDLIST" 84 | func_desc = client.get_function_description(func_name) 85 | compare_function_description( 86 | func_desc, 87 | FUNC_DESC_BAPISDORDER_GETDETAILEDLIST, 88 | ) 89 | 90 | def test_function_description_BS01_SALESORDER_GETDETAIL(self): 91 | func_name = "BS01_SALESORDER_GETDETAIL" 92 | func_desc = client.get_function_description(func_name) 93 | compare_function_description( 94 | func_desc, 95 | FUNC_DESC_BS01_SALESORDER_GETDETAIL, 96 | ) 97 | 98 | def test_stfc_structure(self): 99 | def my_stfc_structure(request_context=None, IMPORTSTRUCT=None, RFCTABLE=None): 100 | """Server function my_stfc_structure with the signature 101 | of ABAP function module STFC_STRUCTURE.""" 102 | 103 | print("stfc structure invoked") 104 | print("request_context", request_context) 105 | if IMPORTSTRUCT is None: 106 | IMPORTSTRUCT = {} 107 | if RFCTABLE is None: 108 | RFCTABLE = [] 109 | ECHOSTRUCT = IMPORTSTRUCT.copy() 110 | ECHOSTRUCT["RFCINT1"] += 1 111 | ECHOSTRUCT["RFCINT2"] += 1 112 | ECHOSTRUCT["RFCINT4"] += 1 113 | if len(RFCTABLE) == 0: 114 | RFCTABLE = [ECHOSTRUCT] 115 | RESPTEXT = f"Python server sends {len(RFCTABLE)} table rows" 116 | print(f"ECHOSTRUCT: {ECHOSTRUCT}") 117 | print(f"RFCTABLE: {RFCTABLE}") 118 | print(f"RESPTEXT: {RESPTEXT}") 119 | 120 | return { 121 | "ECHOSTRUCT": ECHOSTRUCT, 122 | "RFCTABLE": RFCTABLE, 123 | "RESPTEXT": RESPTEXT, 124 | } 125 | 126 | def authentication_check(func_name=False, request_context=None): 127 | if request_context is None: 128 | request_context = {} 129 | print( 130 | f"[js] authorization '{func_name}' request_context", 131 | request_context, 132 | ) 133 | return RCStatus.OK 134 | 135 | def authorization_check(rfcHandle=None, securityAttributes=None): 136 | print(f"[js] authorization for {rfcHandle}, attr {securityAttributes}") 137 | return RCStatus.OK 138 | 139 | # create server 140 | server = Server( 141 | server_params={"dest": "MME_GATEWAY"}, 142 | client_params={"dest": "MME"}, 143 | config={ 144 | "authentication_check": authentication_check, 145 | "authorization_check": authorization_check, 146 | "check_date": False, 147 | "check_time": False, 148 | "server_log": True, 149 | }, 150 | ) 151 | 152 | # expose python function my_stfc_structure as ABAP function STFC_STRUCTURE, 153 | server.add_function("STFC_STRUCTURE", my_stfc_structure) 154 | 155 | # start server 156 | server.start() 157 | 158 | # call ABAP function module which will call Python server 159 | # and return the server response 160 | client = Connection(dest="MME") 161 | result = client.call("ZSERVER_TEST_STFC_STRUCTURE") 162 | 163 | # shutdown server 164 | server.close() 165 | 166 | # check the server response 167 | assert result["RESPTEXT"] == "Python server sends 1 table rows" 168 | assert "ECHOSTRUCT" in result 169 | assert result["ECHOSTRUCT"]["RFCINT1"] == 2 170 | assert result["ECHOSTRUCT"]["RFCINT2"] == 3 171 | assert result["ECHOSTRUCT"]["RFCINT4"] == 5 172 | assert result["ECHOSTRUCT"]["RFCDATE"] == "20230928" 173 | assert result["ECHOSTRUCT"]["RFCTIME"] == "240000" 174 | 175 | def test_trfc(self): 176 | def stfc_write_to_tcpic(request_context=None, RESTART_QNAME="", TCPICDAT=[]): 177 | context = ( 178 | {"error": "No request context"} 179 | if request_context is None 180 | else request_context["server_context"] 181 | ) 182 | print("[js] python function: stfc_write_to_tcpic", f"context {context}") 183 | return {"TCPICDAT": TCPICDAT} 184 | 185 | def onCheckTransaction(rfcHandle, tid): 186 | print("[js] onCheckTransaction", rfcHandle, tid) 187 | return RCStatus.OK 188 | 189 | def onCommitTransaction(rfcHandle, tid): 190 | print("[js] onCommitTransaction", rfcHandle, tid) 191 | return RCStatus.OK 192 | 193 | def onConfirmTransaction(rfcHandle, tid): 194 | print("[js] onConfirmTransaction", rfcHandle, tid) 195 | return RCStatus.OK 196 | 197 | def onRollbackTransaction(rfcHandle, tid): 198 | print("[js] onRollbackTransaction", rfcHandle, tid) 199 | return RCStatus.OK 200 | 201 | def authenticationHandler(function_name, server_context=None): 202 | print(f"[js] authentication for {function_name}, context {server_context}") 203 | return RCStatus.OK 204 | 205 | def authorizationHandler(rfcHandle=None, securityAttributes=None): 206 | print( 207 | f"[js] authorization for {rfcHandle}, security attr {securityAttributes}" 208 | ) 209 | return RCStatus.OK 210 | 211 | try: 212 | # Create server 213 | server = Server( 214 | server_params={"dest": "MME_GATEWAY"}, 215 | client_params={"dest": "MME"}, 216 | config={"check_date": False, "check_time": False, "server_log": False}, 217 | ) 218 | 219 | # ABAP function used to send IDocs via tRFC/qRFC 220 | server.add_function("STFC_WRITE_TO_TCPIC", stfc_write_to_tcpic) 221 | 222 | # Register the RFC transaction handlers. 223 | server.transaction_rfc_init( 224 | sysId="MME", 225 | transactionHandler={ 226 | "check": onCheckTransaction, 227 | "commit": onCommitTransaction, 228 | "rollback": onRollbackTransaction, 229 | "confirm": onConfirmTransaction, 230 | }, 231 | ) 232 | 233 | # Start server 234 | server.start() 235 | 236 | # call RSARFCT0 237 | client = Connection(dest="MME") 238 | tcall = "00001" 239 | ncall = client.call("ZSERVER_TEST_TRFC", NCALL=tcall)["EV_NCALL"] 240 | client.close() 241 | assert ncall == tcall 242 | 243 | # receive queues from abap system 244 | sleep(5) 245 | 246 | except Exception as ex: 247 | print(ex) 248 | finally: 249 | # Shutdown server 250 | server.close() 251 | 252 | 253 | def teardown(): 254 | server.close() 255 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # Project documentation build configuration file, created by 3 | # sphinx-quickstart on Fri Apr 8 16:06:08 2011. 4 | # 5 | # This file is execfile()d with the current directory set to its containing dir. 6 | # 7 | # Note that not all possible configuration values are present in this 8 | # autogenerated file. 9 | # 10 | # All configuration values have a default; values that are commented out 11 | # serve to show the default. 12 | 13 | import os, pyrfc 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # sys.path.insert(0, os.path.abspath('.')) 19 | 20 | # -- General configuration ----------------------------------------------------- 21 | 22 | # If your documentation needs a minimal Sphinx version, state it here. 23 | # needs_sphinx = '1.0' 24 | 25 | # Add any Sphinx extension module names here, as strings. They can be extensions 26 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 27 | extensions = [ 28 | "sphinx.ext.napoleon", 29 | "sphinx.ext.autodoc", 30 | "sphinx.ext.autosummary", 31 | "sphinx.ext.todo", 32 | "sphinx.ext.ifconfig", 33 | ] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ["_templates"] 37 | 38 | # The suffix of source filenames. 39 | source_suffix = ".rst" 40 | 41 | # The encoding of source files. 42 | # source_encoding = 'utf-8-sig' 43 | 44 | # The master toctree document. 45 | master_doc = "index" 46 | 47 | # General information about the project. 48 | project = u"pyrfc" 49 | copyright = u"2014, SAP" 50 | 51 | # The version info for the project you're documenting, acts as replacement for 52 | # |version| and |release|, also used in various other places throughout the 53 | # built documents. 54 | # 55 | # CUSTOM: read the version from the installed pyrfc build 56 | release = version = pyrfc.__version__ 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | # language = None 61 | 62 | # There are two options for replacing |today|: either, you set today to some 63 | # non-false value, then it is used: 64 | # today = '' 65 | # Else, today_fmt is used as the format for a strftime call. 66 | # today_fmt = '%B %d, %Y' 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | exclude_patterns = ["_build"] 71 | 72 | # The reST default role (used for this markup: `text`) to use for all documents. 73 | # default_role = None 74 | 75 | # If true, '()' will be appended to :func: etc. cross-reference text. 76 | # add_function_parentheses = True 77 | 78 | # If true, the current module name will be prepended to all description 79 | # unit titles (such as .. function::). 80 | # add_module_names = True 81 | 82 | # If true, sectionauthor and moduleauthor directives will be shown in the 83 | # output. They are ignored by default. 84 | # show_authors = False 85 | 86 | # The name of the Pygments (syntax highlighting) style to use. 87 | pygments_style = "sphinx" 88 | 89 | # A list of ignored prefixes for module index sorting. 90 | # modindex_common_prefix = [] 91 | 92 | # Autodoc configuration 93 | # autodoc_default_flags = ['members', 'undoc-members'] 94 | 95 | # to-do configuration 96 | todo_include_todos = True 97 | 98 | # As it is not possible to comment class attributes in cython, we will 99 | # have to hack something... 100 | # cf. http://gavinroy.com/automagical-docstring-linking-in-sphinx 101 | def process_docstring(app, what, name, obj, options, lines): 102 | # lines.append("app:{},what:{},name:{},obj:{}".format( app, what, name, obj )) 103 | # lines.append("name:'{}'".format(name)) 104 | 105 | # loop through each line in the docstring and replace |class| with 106 | # the classname 107 | # for i in xrange(len(lines)): 108 | # lines[i] = lines[i].replace('connection', 'hurz') 109 | 110 | if name == "pyrfc.Connection.rstrip": 111 | lines.append( 112 | u"If True, returned strings from :meth:`~Connection.call` are right stripped." 113 | ) 114 | lines.append(u"For details see :ref:`client-connectionconfig-rstrip`.") 115 | elif name == "pyrfc.Connection.config.return_import_params": 116 | lines.append( 117 | u"If True, :meth:`~Connection.call` also returns parameters of type IMPORT." 118 | ) 119 | lines.append( 120 | u"For details see :ref:`client-connectionconfig-returnimportparams`." 121 | ) 122 | elif name == "pyrfc.Connection.config.dtime": 123 | lines.append( 124 | u"If True, :meth:`~Connection.call` also returns ABAP DATE and TIME fields as Python datetime objects." 125 | ) 126 | lines.append( 127 | u"For details see :ref:`client-connectionconfig-dtime`." 128 | ) 129 | 130 | def setup(app): 131 | # Setting a hook for processing docstrings 132 | app.connect("autodoc-process-docstring", process_docstring) 133 | 134 | 135 | # -- Options for HTML output --------------------------------------------------- 136 | 137 | # The theme to use for HTML and HTML Help pages. See the documentation for 138 | # a list of builtin themes. 139 | # html_theme = 'default' 140 | html_theme = "agogo" 141 | 142 | # Theme options are theme-specific and customize the look and feel of a theme 143 | # further. For a list of options available for each theme, see the 144 | # documentation. 145 | # html_theme_options = {} 146 | # cf. http://sphinx.pocoo.org/theming.html#builtin-themes 147 | html_theme_options = { 148 | "pagewidth": "70em", 149 | "documentwidth": "55em", 150 | "sidebarwidth": "15em", 151 | } 152 | 153 | # Add any paths that contain custom themes here, relative to this directory. 154 | # html_theme_path = [] 155 | 156 | # The name for this set of Sphinx documents. If None, it defaults to 157 | # " v documentation". 158 | # html_title = None 159 | 160 | # A shorter title for the navigation bar. Default is the same as html_title. 161 | # html_short_title = None 162 | 163 | # The name of an image file (relative to this directory) to place at the top 164 | # of the sidebar. 165 | # html_logo = None 166 | 167 | # The name of an image file (within the static path) to use as favicon of the 168 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 169 | # pixels large. 170 | # html_favicon = None 171 | 172 | # Add any paths that contain custom static files (such as style sheets) here, 173 | # relative to this directory. They are copied after the builtin static files, 174 | # so a file named "default.css" will overwrite the builtin "default.css". 175 | html_static_path = ["_static"] 176 | 177 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 178 | # using the given strftime format. 179 | html_last_updated_fmt = "%Y-%m-%d %H:%M" 180 | 181 | # If true, SmartyPants will be used to convert quotes and dashes to 182 | # typographically correct entities. 183 | html_use_smartypants = True 184 | 185 | # Custom sidebar templates, maps document names to template names. 186 | # html_sidebars = {} 187 | 188 | # Additional templates that should be rendered to pages, maps page names to 189 | # template names. 190 | # html_additional_pages = {} 191 | 192 | # If false, no module index is generated. 193 | # html_domain_indices = True 194 | 195 | # If false, no index is generated. 196 | # html_use_index = True 197 | 198 | # If true, the index is split into individual pages for each letter. 199 | # html_split_index = False 200 | 201 | # If true, links to the reST sources are added to the pages. 202 | # html_show_sourcelink = True 203 | 204 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 205 | # html_show_sphinx = True 206 | 207 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 208 | # html_show_copyright = True 209 | 210 | # If true, an OpenSearch description file will be output, and all pages will 211 | # contain a tag referring to it. The value of this option must be the 212 | # base URL from which the finished HTML is served. 213 | # html_use_opensearch = '' 214 | 215 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 216 | # html_file_suffix = None 217 | 218 | # Output file base name for HTML help builder. 219 | htmlhelp_basename = "pyrfcdoc" 220 | 221 | 222 | # -- Options for LaTeX output -------------------------------------------------- 223 | 224 | # The paper size ('letter' or 'a4'). 225 | latex_paper_size = "a4" 226 | 227 | # The font size ('10pt', '11pt' or '12pt'). 228 | # latex_font_size = '10pt' 229 | 230 | # Grouping the document tree into LaTeX files. List of tuples 231 | # (source start file, target name, title, author, documentclass [howto/manual]). 232 | latex_documents = [ 233 | ("index", "pyrfc-%s.tex" % release, u"pyrfc Documentation", u"SAP", "manual") 234 | ] 235 | 236 | # The name of an image file (relative to this directory) to place at the top of 237 | # the title page. 238 | # latex_logo = None 239 | 240 | # For "manual" documents, if this is true, then toplevel headings are parts, 241 | # not chapters. 242 | # latex_use_parts = False 243 | 244 | # If true, show page references after internal links. 245 | # latex_show_pagerefs = False 246 | 247 | # If true, show URL addresses after external links. 248 | # latex_show_urls = False 249 | 250 | # Additional stuff for the LaTeX preamble. 251 | # latex_preamble = '' 252 | 253 | # Documents to append as an appendix to all manuals. 254 | # latex_appendices = [] 255 | 256 | # If false, no module index is generated. 257 | # latex_domain_indices = True 258 | 259 | 260 | # -- Options for manual page output -------------------------------------------- 261 | 262 | # One entry per manual page. List of tuples 263 | # (source start file, name, description, authors, manual section). 264 | man_pages = [("index", "pyrfc", u"pyrfc Documentation", [u"SAP"], 1)] 265 | -------------------------------------------------------------------------------- /tests/test_connection.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 SAP SE Srdjan Boskovic 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import datetime 6 | import socket 7 | import sys 8 | from contextlib import suppress 9 | 10 | from tests.config import CONNECTION_DEST as paramsdest 11 | from tests.config import CONNECTION_PARAMS as params 12 | from tests.config import UNICODETEST, latest_python_version 13 | 14 | with suppress(ModuleNotFoundError): 15 | import tomllib 16 | 17 | import pytest 18 | from pyrfc import Connection, ExternalRuntimeError, RFCError, __version__ 19 | 20 | 21 | class TestConnection: 22 | def setup_method(self): 23 | self.conn = Connection(**paramsdest) 24 | assert self.conn.alive 25 | 26 | def teardown_method(self): 27 | self.conn.close() 28 | assert not self.conn.alive 29 | 30 | def test_sdk_version_and_options_getters(self): 31 | version = self.conn.version 32 | assert "major" in version 33 | assert "minor" in version 34 | assert "patchLevel" in version 35 | assert all( 36 | attr in self.conn.options 37 | for attr in ( 38 | "dtime", 39 | "return_import_params", 40 | "rstrip", 41 | ) 42 | ) 43 | 44 | @pytest.mark.skipif( 45 | "tomllib" not in sys.modules or sys.version_info < latest_python_version, 46 | reason="pyrfc version check on latest python only", 47 | ) 48 | def test_pyrfc_version(self): 49 | with open("pyproject.toml", "rb") as file: 50 | pyproject = tomllib.load(file) 51 | package_name = pyproject["project"]["name"] 52 | version = pyproject["project"]["version"] 53 | assert package_name == "pyrfc" 54 | if __version__ != "0.0.0": 55 | assert version == __version__ 56 | 57 | def test_connection_info(self): 58 | connection_info = self.conn.get_connection_attributes() 59 | info_keys = ( 60 | "dest", 61 | "host", 62 | "partnerHost", 63 | "sysNumber", 64 | "sysId", 65 | "client", 66 | "user", 67 | "language", 68 | "trace", 69 | "isoLanguage", 70 | "codepage", 71 | "partnerCodepage", 72 | "rfcRole", 73 | "type", 74 | "partnerType", 75 | "rel", 76 | "partnerRel", 77 | "kernelRel", 78 | "cpicConvId", 79 | "progName", 80 | "partnerBytesPerChar", 81 | "partnerIP", 82 | "partnerIPv6", 83 | # 'reserved' 84 | ) 85 | assert all(attr in connection_info for attr in info_keys) 86 | 87 | def test_connection_info_attributes(self): 88 | attributes = self.conn.get_connection_attributes() 89 | assert attributes["client"] == str(params["client"]) 90 | assert attributes["host"] == str(socket.gethostname()) 91 | assert attributes["isoLanguage"] == str(params["lang"].upper()) 92 | # Only valid for direct logon systems: 93 | # self.assertEqual(attributes['sysNumber'], str(params['sysnr'])) 94 | assert attributes["user"] == str(params["user"].upper()) 95 | assert attributes["rfcRole"] == "C" 96 | 97 | def test_connection_info_disconnected(self): 98 | self.conn.close() 99 | assert not self.conn.alive 100 | connection_info = self.conn.get_connection_attributes() 101 | assert connection_info == {} 102 | 103 | def test_reopen(self): 104 | assert self.conn.alive 105 | self.conn.reopen() 106 | assert self.conn.alive 107 | self.conn.close() 108 | assert not self.conn.alive 109 | self.conn.reopen() 110 | assert self.conn.alive 111 | 112 | def test_call_over_closed_connection(self): 113 | conn = Connection(config={"rstrip": False}, **params) 114 | conn.close() 115 | assert conn.alive is False 116 | hello = "Hällo SAP!" 117 | with pytest.raises(RFCError) as ex: 118 | conn.call("STFC_CONNECTION", REQUTEXT=hello) 119 | error = ex.value 120 | assert ( 121 | error.args[0] 122 | == "Remote function module 'STFC_CONNECTION' invocation rejected because the connection is closed" # noqa: E501 123 | ) 124 | 125 | def test_ping(self): 126 | assert self.conn.alive 127 | self.conn.ping() 128 | self.conn.close() 129 | with pytest.raises(ExternalRuntimeError) as ex: 130 | self.conn.ping() 131 | error = ex.value 132 | assert error.code == 13 133 | assert error.key == "RFC_INVALID_HANDLE" 134 | assert error.message in [ 135 | "An invalid handle 'RFC_CONNECTION_HANDLE' was passed to the API call", 136 | "An invalid handle was passed to the API call", 137 | ] 138 | 139 | def test_RFM_name_string(self): 140 | res = self.conn.call( 141 | "STFC_CONNECTION", 142 | REQUTEXT=UNICODETEST, 143 | ) 144 | assert res["ECHOTEXT"] == UNICODETEST 145 | 146 | def test_RFM_name_unicode(self): 147 | res = self.conn.call( 148 | "STFC_CONNECTION", 149 | REQUTEXT=UNICODETEST, 150 | ) 151 | assert res["ECHOTEXT"] == UNICODETEST 152 | 153 | def test_RFM_name_invalid_type(self): 154 | with pytest.raises(Exception) as ex: 155 | self.conn.call(123) 156 | error = ex.value 157 | assert error.args == ( 158 | "Remote function module name must be unicode string, received:", 159 | 123, 160 | int, 161 | ) 162 | 163 | def test_STFC_returns_structure_and_table(self): 164 | IMPORTSTRUCT = { 165 | "RFCFLOAT": 1.23456789, 166 | "RFCCHAR1": "A", 167 | "RFCCHAR2": "BC", 168 | "RFCCHAR4": "DEFG", 169 | "RFCINT1": 1, 170 | "RFCINT2": 2, 171 | "RFCINT4": 345, 172 | "RFCHEX3": bytes(b"\x01\x02\x03"), 173 | "RFCTIME": "121120", 174 | "RFCDATE": "20140101", 175 | "RFCDATA1": "1DATA1", 176 | "RFCDATA2": "DATA222", 177 | } 178 | INPUTROWS = 10 179 | IMPORTTABLE = [] 180 | for idx in range(INPUTROWS): 181 | row = IMPORTSTRUCT 182 | row["RFCINT1"] = idx 183 | IMPORTTABLE.append(row) 184 | res = self.conn.call( 185 | "STFC_STRUCTURE", 186 | IMPORTSTRUCT=IMPORTSTRUCT, 187 | RFCTABLE=IMPORTTABLE, 188 | ) 189 | # ECHOSTRUCT match IMPORTSTRUCT 190 | for attr in IMPORTSTRUCT: 191 | assert res["ECHOSTRUCT"][attr] == IMPORTSTRUCT[attr] 192 | 193 | # check if row added 194 | assert len(res["RFCTABLE"]) == INPUTROWS + 1 195 | 196 | # output table match import table 197 | for idx in range(INPUTROWS): 198 | row_in = IMPORTTABLE[idx] 199 | row_out = res["RFCTABLE"][idx] 200 | assert row_in == row_out 201 | 202 | # added row match incremented IMPORTSTRUCT 203 | added_row = res["RFCTABLE"][INPUTROWS] 204 | assert added_row["RFCFLOAT"] == IMPORTSTRUCT["RFCFLOAT"] + 1 205 | assert added_row["RFCINT1"] == IMPORTSTRUCT["RFCINT1"] + 1 206 | assert added_row["RFCINT2"] == IMPORTSTRUCT["RFCINT2"] + 1 207 | assert added_row["RFCINT4"] == IMPORTSTRUCT["RFCINT4"] + 1 208 | 209 | def test_STFC_STRUCTURE(self): 210 | # STFC_STRUCTURE Inhomogene Struktur 211 | imp = { 212 | "RFCFLOAT": 1.23456789, 213 | "RFCINT2": 0x7FFE, 214 | "RFCINT1": 0x7F, 215 | "RFCCHAR4": "bcde", 216 | "RFCINT4": 0x7FFFFFFE, 217 | "RFCHEX3": "fgh".encode("utf-8"), 218 | "RFCCHAR1": "a", 219 | "RFCCHAR2": "ij", 220 | "RFCTIME": "123456", # datetime.time(12,34,56), 221 | "RFCDATE": "20161231", # datetime.date(2011,10,17), 222 | "RFCDATA1": "k" * 50, 223 | "RFCDATA2": "l" * 50, 224 | } 225 | out = { 226 | "RFCFLOAT": imp["RFCFLOAT"] + 1, # type: ignore 227 | "RFCINT2": imp["RFCINT2"] + 1, # type: ignore 228 | "RFCINT1": imp["RFCINT1"] + 1, # type: ignore 229 | "RFCINT4": imp["RFCINT4"] + 1, # type: ignore 230 | "RFCHEX3": b"\xf1\xf2\xf3", 231 | "RFCCHAR1": "X", 232 | "RFCCHAR2": "YZ", 233 | "RFCDATE": str(datetime.date.today()).replace( 234 | "-", 235 | "", 236 | ), 237 | "RFCDATA1": "k" * 50, 238 | "RFCDATA2": "l" * 50, 239 | } 240 | table = [] 241 | xtable = [] 242 | records = [ 243 | "1111", 244 | "2222", 245 | "3333", 246 | "4444", 247 | "5555", 248 | ] 249 | for rid in records: 250 | imp["RFCCHAR4"] = rid 251 | table.append(imp) 252 | xtable.append(imp) 253 | # print 'table len', len(table), len(xtable) 254 | res = self.conn.call( 255 | "STFC_STRUCTURE", 256 | IMPORTSTRUCT=imp, 257 | RFCTABLE=xtable, 258 | ) 259 | # print 'table len', len(table), len(xtable) 260 | assert res["RESPTEXT"].startswith("SAP") 261 | # assert res['ECHOSTRUCT'] == imp 262 | assert len(res["RFCTABLE"]) == 1 + len(table) 263 | for idx in res["ECHOSTRUCT"]: 264 | assert res["ECHOSTRUCT"][idx] == imp[idx] 265 | for idx in res["RFCTABLE"][5]: 266 | # dont compare variable system id and server time 267 | if idx not in ["RFCCHAR4", "RFCTIME"]: 268 | assert res["RFCTABLE"][5][idx] == out[idx] 269 | 270 | def test_STFC_CHANGING(self): 271 | # STFC_CHANGING example with CHANGING parameters 272 | start_value = 33 273 | counter = 88 274 | res = self.conn.call( 275 | "STFC_CHANGING", 276 | START_VALUE=start_value, 277 | COUNTER=counter, 278 | ) 279 | assert res["COUNTER"] == counter + 1 280 | assert res["RESULT"] == start_value + counter 281 | --------------------------------------------------------------------------------