abap-adt-py 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 timkoehne
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: abap-adt-py
3
+ Version: 0.1.0
4
+ Summary: A Python library for interacting with SAP systems using the ADT (ABAP Development Tools) protocol.
5
+ Author-email: Tim Köhne <20153452+timkoehne@users.noreply.github.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2023 timkoehne
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Project-URL: Homepage, https://github.com/timkoehne/abap-adt-py
28
+ Project-URL: Bug Tracker, https://github.com/timkoehne/abap-adt-py/issues
29
+ Classifier: Programming Language :: Python :: 3
30
+ Classifier: Programming Language :: Python :: 3.7
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Operating System :: OS Independent
33
+ Requires-Python: >=3.7
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENSE
36
+ Requires-Dist: requests>=2.25
37
+ Requires-Dist: typing-extensions>=4.7.1
38
+ Dynamic: license-file
File without changes
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "abap-adt-py"
7
+ version = "0.1.0"
8
+ description = "A Python library for interacting with SAP systems using the ADT (ABAP Development Tools) protocol."
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ authors = [
12
+ { name = "Tim Köhne", email = "20153452+timkoehne@users.noreply.github.com" }
13
+ ]
14
+ requires-python = ">=3.7"
15
+ dependencies = [
16
+ "requests>=2.25",
17
+ "typing-extensions>=4.7.1",
18
+ ]
19
+ classifiers = [
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.7",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ ]
25
+
26
+ [project.urls]
27
+ "Homepage" = "https://github.com/timkoehne/abap-adt-py"
28
+ "Bug Tracker" = "https://github.com/timkoehne/abap-adt-py/issues"
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,131 @@
1
+ import requests
2
+ from requests.auth import HTTPBasicAuth
3
+
4
+ from .compat_typing import Literal, List, Dict
5
+ from .api.prettyprint import PrettyPrintSettings, prettyprint, set_pretty_printer_settings
6
+ from .api.create import create, create_test_class_include
7
+ from .api.activate import activate
8
+ from .api.create import ObjectTypes
9
+ from .api.delete import delete
10
+ from .api.lock import lock, unlock
11
+ from .api.login import login
12
+ from .api.content import get_object_source, set_object_source
13
+ from .api.search import search_object
14
+ from .api.unittest import UnitTestAlert, UnittestFlags, run_unit_test
15
+ from .http_request import HttpRequestParameters
16
+
17
+
18
+ class AdtClient:
19
+ sap_host: str
20
+ csrf_token: str = "fetch"
21
+ request_number: int = 0
22
+ statefulness: Literal["stateless", "stateful"] = "stateless"
23
+
24
+ def __init__(
25
+ self, sap_host: str, username: str, password: str, client: str, language: str
26
+ ):
27
+ self.username = username
28
+ self.session = requests.Session()
29
+ self.session.auth = HTTPBasicAuth(username, password)
30
+ self.sap_host = sap_host
31
+ self.client = client
32
+ self.language = language
33
+
34
+ def build_request_parameters(self) -> HttpRequestParameters:
35
+ http_request_parameters: HttpRequestParameters = {
36
+ "host": self.sap_host,
37
+ "csrf_token": self.csrf_token,
38
+ "statefulness": self.statefulness,
39
+ "request_number": self.request_number,
40
+ "session": self.session,
41
+ }
42
+ self.request_number += 1
43
+ return http_request_parameters
44
+
45
+ def login(self) -> bool:
46
+ http_request_parameters = self.build_request_parameters()
47
+ csrf_token = login(http_request_parameters)
48
+ if csrf_token:
49
+ self.csrf_token = csrf_token
50
+ return True
51
+ else:
52
+ raise Exception("Login failed.")
53
+
54
+ def search_object(self, query: str, max_results: int = 1) -> List[Dict[str, str]]:
55
+ http_request_parameters = self.build_request_parameters()
56
+ elements = search_object(http_request_parameters, query, max_results)
57
+ return elements
58
+
59
+ def get_object_source(
60
+ self, object_uri: str, version: Literal["active", "inactive"] = "active"
61
+ ) -> str:
62
+ http_request_parameters = self.build_request_parameters()
63
+ response = get_object_source(http_request_parameters, object_uri, version)
64
+ return response
65
+
66
+ def activate(self, object_name: str, object_uri: str) -> bool:
67
+ http_request_parameters = self.build_request_parameters()
68
+ response = activate(http_request_parameters, object_name, object_uri)
69
+ return response
70
+
71
+ def lock(self, object_uri: str) -> str:
72
+ self.statefulness = "stateful"
73
+ http_request_parameters = self.build_request_parameters()
74
+ response = lock(http_request_parameters, object_uri)
75
+ return response
76
+
77
+ def unlock(self, object_uri: str, lock_handle: str) -> bool:
78
+ http_request_parameters = self.build_request_parameters()
79
+ response = unlock(http_request_parameters, object_uri, lock_handle)
80
+ self.statefulness = "stateless"
81
+ return response
82
+
83
+ def set_object_source(
84
+ self, object_uri: str, source_code: str, lock_handle: str
85
+ ) -> bool:
86
+ http_request_parameters = self.build_request_parameters()
87
+ response = set_object_source(
88
+ http_request_parameters, object_uri, source_code, lock_handle
89
+ )
90
+ return response
91
+
92
+ def run_unit_test(
93
+ self, object_uri: str, unit_test_flags: UnittestFlags = UnittestFlags()
94
+ ) -> List[UnitTestAlert]:
95
+ http_request_parameters = self.build_request_parameters()
96
+ response = run_unit_test(http_request_parameters, object_uri, unit_test_flags)
97
+ return response
98
+
99
+ def delete(self, object_uri: str, lock_handle: str) -> bool:
100
+ http_request_parameters = self.build_request_parameters()
101
+ response = delete(http_request_parameters, object_uri, lock_handle)
102
+ return response
103
+
104
+ def create(
105
+ self, object_type: ObjectTypes, name: str, parent: str, description: str
106
+ ) -> bool:
107
+ http_request_parameters = self.build_request_parameters()
108
+ response = create(
109
+ http_request_parameters,
110
+ object_type,
111
+ name,
112
+ parent,
113
+ description,
114
+ self.username,
115
+ )
116
+ return response
117
+
118
+ def create_test_class_include(self, class_name: str, lock_handle: str) -> bool:
119
+ http_request_parameters = self.build_request_parameters()
120
+ response = create_test_class_include(http_request_parameters, class_name, lock_handle)
121
+ return response
122
+
123
+ def prettyprint(self, src: str) -> str:
124
+ http_request_parameters = self.build_request_parameters()
125
+ response = prettyprint(http_request_parameters, src)
126
+ return response
127
+
128
+ def prettyprint_settings(self, settings: PrettyPrintSettings) -> bool:
129
+ http_request_parameters = self.build_request_parameters()
130
+ response = set_pretty_printer_settings(http_request_parameters, settings)
131
+ return response
File without changes
@@ -0,0 +1,33 @@
1
+ from ..http_request import HttpRequestParameters, request
2
+ from ..response_parsing import find_xml_element_attributes, find_xml_elements_attributes
3
+
4
+
5
+ def activate(
6
+ http_request_parameters: HttpRequestParameters, object_name: str, object_uri: str
7
+ ) -> bool:
8
+
9
+ body = f"""
10
+ <?xml version="1.0" encoding="UTF-8"?>
11
+ <adtcore:objectReferences xmlns:adtcore="http://www.sap.com/adt/core">
12
+ <adtcore:objectReference adtcore:uri="{object_uri}" adtcore:name="{object_name}"/>
13
+ </adtcore:objectReferences>
14
+ """
15
+
16
+ response = request(
17
+ http_request_parameters,
18
+ uri="/sap/bc/adt/activation",
19
+ params={"method": "activate", "preauditRequested": "true"},
20
+ body=body,
21
+ method="POST",
22
+ content_type="application/xml",
23
+ )
24
+ print(response.text)
25
+ properties = find_xml_element_attributes(response.text, "chkl:properties")
26
+ if (
27
+ properties["activationExecuted"] == "true"
28
+ or properties["generationExecuted"] == "true"
29
+ ):
30
+ return True
31
+ else:
32
+ msg_elements = find_xml_elements_attributes(response.text, "msg")
33
+ raise Exception(f"{response.status_code} - Activation failed.\n{msg_elements}")
@@ -0,0 +1,52 @@
1
+ from ..compat_typing import Literal
2
+ from ..http_request import HttpRequestParameters, request
3
+
4
+
5
+ def get_object_source(
6
+ http_request_parameters: HttpRequestParameters,
7
+ object_uri: str,
8
+ version: Literal["active", "inactive"] = "active",
9
+ ) -> str:
10
+
11
+ params = {}
12
+ if version:
13
+ params["version"] = version
14
+
15
+ response = request(
16
+ http_request_parameters=http_request_parameters,
17
+ uri=object_uri,
18
+ method="GET",
19
+ body="",
20
+ params=params,
21
+ content_type="application/xml",
22
+ )
23
+
24
+ if response.status_code == 200:
25
+ return response.text
26
+ else:
27
+ raise Exception(
28
+ f"{response.status_code} - Failed to get object source.\n{response.text}"
29
+ )
30
+
31
+
32
+ def set_object_source(
33
+ http_request_parameters: HttpRequestParameters,
34
+ object_uri: str,
35
+ source_code: str,
36
+ lock_handle: str,
37
+ ) -> bool:
38
+ response = request(
39
+ http_request_parameters=http_request_parameters,
40
+ uri=object_uri,
41
+ method="PUT",
42
+ body=source_code,
43
+ params={"lockHandle": lock_handle},
44
+ content_type="text/plain; charset=utf-8",
45
+ )
46
+
47
+ if response.status_code == 200:
48
+ return True
49
+ else:
50
+ raise Exception(
51
+ f"{response.status_code} - Failed to set source code for {object_uri}\n{response.text}"
52
+ )
@@ -0,0 +1,181 @@
1
+ from ..compat_typing import Literal, TypeAlias, Dict, TypedDict
2
+ from ..http_request import HttpRequestParameters, request
3
+
4
+
5
+ class CreateableTypeDetails(TypedDict):
6
+ path: str
7
+ xml_name: str
8
+ xml_namespace: str
9
+
10
+
11
+ ObjectTypes: TypeAlias = Literal[
12
+ "PROG/P",
13
+ "CLAS/OC",
14
+ "TABL/DT",
15
+ "INTF/OI",
16
+ "PROG/I",
17
+ "FUGR/F",
18
+ "FUGR/FF",
19
+ "MSAG/N",
20
+ "DCLS/DL",
21
+ "DDLS/DF",
22
+ "DDLX/EX",
23
+ "DTEL/DE",
24
+ ]
25
+
26
+ CreateableTypes: TypeAlias = Dict[ObjectTypes, CreateableTypeDetails]
27
+ CREATEABLE_TYPES: CreateableTypes = {
28
+ "PROG/P": {
29
+ "path": "/sap/bc/adt/programs/programs",
30
+ "xml_name": "program:abapProgram",
31
+ "xml_namespace": 'xmlns:program="http://www.sap.com/adt/programs/programs"',
32
+ },
33
+ "CLAS/OC": {
34
+ "path": "/sap/bc/adt/oo/classes",
35
+ "xml_name": "class:abapClass",
36
+ "xml_namespace": 'xmlns:class="http://www.sap.com/adt/oo/classes"',
37
+ },
38
+ "TABL/DT": {
39
+ "path": "/sap/bc/adt/ddic/tables",
40
+ "xml_name": "blue:blueSource",
41
+ "xml_namespace": 'xmlns:blue="http://www.sap.com/wbobj/blue"',
42
+ },
43
+ "INTF/OI": {
44
+ "path": "/sap/bc/adt/oo/interfaces",
45
+ "xml_name": "intf:abapInterface",
46
+ "xml_namespace": 'xmlns:intf="http://www.sap.com/adt/oo/interfaces"',
47
+ },
48
+ "PROG/I": {
49
+ "path": "/sap/bc/adt/programs/includes",
50
+ "xml_name": "include:abapInclude",
51
+ "xml_namespace": 'xmlns:include="http://www.sap.com/adt/programs/includes"',
52
+ },
53
+ "FUGR/F": {
54
+ "path": "/sap/bc/adt/functions/groups",
55
+ "xml_name": "group:abapFunctionGroup",
56
+ "xml_namespace": 'xmlns:group="http://www.sap.com/adt/functions/groups"',
57
+ },
58
+ "FUGR/FF": {
59
+ "path": "/sap/bc/adt/functions/groups/{}/fmodules",
60
+ "xml_name": "fmodule:abapFunctionModule",
61
+ "xml_namespace": 'xmlns:fmodule="http://www.sap.com/adt/functions/fmodules"',
62
+ },
63
+ "MSAG/N": {
64
+ "path": "/sap/bc/adt/messageclass",
65
+ "xml_name": "mc:messageClass",
66
+ "xml_namespace": 'xmlns:mc="http://www.sap.com/adt/MessageClass"',
67
+ },
68
+ "DCLS/DL": {
69
+ "path": "/sap/bc/adt/acm/dcl/sources",
70
+ "xml_name": "dcl:dclSource",
71
+ "xml_namespace": 'xmlns:dcl="http://www.sap.com/adt/acm/dclsources"',
72
+ },
73
+ "DDLS/DF": {
74
+ "path": "/sap/bc/adt/ddic/ddl/sources",
75
+ "xml_name": "ddl:ddlSource",
76
+ "xml_namespace": 'xmlns:ddl="http://www.sap.com/adt/ddic/ddlsources"',
77
+ },
78
+ "DDLX/EX": {
79
+ "path": "/sap/bc/adt/ddic/ddlx/sources",
80
+ "xml_name": "ddlx:ddlxSource",
81
+ "xml_namespace": 'xmlns:ddlx="http://www.sap.com/adt/ddic/ddlxsources"',
82
+ },
83
+ "DTEL/DE": {
84
+ "path": "/sap/bc/adt/ddic/dataelements",
85
+ "xml_name": "blue:wbobj",
86
+ "xml_namespace": 'xmlns:blue="http://www.sap.com/wbobj/dictionary/dtel"',
87
+ },
88
+ }
89
+
90
+
91
+ def _build_body(
92
+ description: str, name: str, package: str, owner: str, object_type: ObjectTypes
93
+ ) -> str:
94
+
95
+ if object_type in ["FUGR/FF"]:
96
+ parent_ref = f"""
97
+ <adtcore:containerRef adtcore:name="{owner}"
98
+ adtcore:type="FUGR/F"
99
+ adtcore:uri="{package}" />"""
100
+ else:
101
+ parent_ref = f"""
102
+ <adtcore:packageRef adtcore:name="{package}"/>
103
+ """
104
+
105
+ creatable_object = CREATEABLE_TYPES[object_type]
106
+
107
+ return f"""
108
+ <?xml version="1.0" encoding="UTF-8"?>
109
+ <{creatable_object["xml_name"]} {creatable_object["xml_namespace"]}
110
+ xmlns:adtcore="http://www.sap.com/adt/core"
111
+ adtcore:description="{description}"
112
+ adtcore:name="{name}" adtcore:type="{object_type}"
113
+ adtcore:responsible="{owner}" >
114
+ {parent_ref}
115
+ </{creatable_object["xml_name"]}>
116
+ """
117
+
118
+
119
+ def create(
120
+ http_request_parameters: HttpRequestParameters,
121
+ object_type: ObjectTypes,
122
+ name: str,
123
+ parent: str,
124
+ description: str,
125
+ owner: str,
126
+ ) -> bool:
127
+
128
+ object_uri = CREATEABLE_TYPES[object_type]["path"]
129
+
130
+ # noop for most objects but eg. function modules need their function group in their object uri
131
+ object_uri = object_uri.format(parent)
132
+
133
+ body = _build_body(
134
+ description=description,
135
+ name=name,
136
+ package=parent,
137
+ object_type=object_type,
138
+ owner=owner,
139
+ )
140
+
141
+ response = request(
142
+ http_request_parameters=http_request_parameters,
143
+ uri=object_uri,
144
+ method="POST",
145
+ body=body,
146
+ params={},
147
+ content_type="application/*",
148
+ )
149
+
150
+ if 200 <= response.status_code <= 300:
151
+ return True
152
+ else:
153
+ raise Exception(
154
+ f"{response.status_code} - Failed to create object {name}\n{response.text}"
155
+ )
156
+
157
+
158
+ def create_test_class_include(
159
+ http_request_parameters: HttpRequestParameters, class_name: str, lock_handle: str
160
+ ) -> bool:
161
+
162
+ body = f"""
163
+ <?xml version="1.0" encoding="UTF-8"?>
164
+ <class:abapClassInclude xmlns:class="http://www.sap.com/adt/oo/classes"
165
+ xmlns:adtcore="http://www.sap.com/adt/core" adtcore:name="{class_name}" class:includeType="testclasses"/>
166
+ """
167
+
168
+ response = request(
169
+ http_request_parameters=http_request_parameters,
170
+ uri=f"/sap/bc/adt/oo/classes/{class_name}/includes",
171
+ method="POST",
172
+ body=body,
173
+ params={"lockHandle": lock_handle},
174
+ )
175
+
176
+ if 200 <= response.status_code <= 300:
177
+ return True
178
+ else:
179
+ raise Exception(
180
+ f"{response.status_code} - Failed to create testclass for {class_name}\n{response.text}"
181
+ )
@@ -0,0 +1,17 @@
1
+ from ..http_request import HttpRequestParameters, request
2
+
3
+
4
+ def delete(http_request_parameters: HttpRequestParameters, object_uri: str, lock_handle: str) -> bool:
5
+ response = request(
6
+ http_request_parameters=http_request_parameters,
7
+ uri=object_uri,
8
+ body="",
9
+ params={"lockHandle": lock_handle},
10
+ method="DELETE",
11
+ )
12
+ if response.status_code == 200:
13
+ return True
14
+ else:
15
+ raise Exception(
16
+ f"{response.status_code} Failed to delete {object_uri}.\n{response.text}"
17
+ )
@@ -0,0 +1,38 @@
1
+ from ..http_request import HttpRequestParameters, request
2
+ from ..response_parsing import find_xml_element_text
3
+
4
+
5
+ def lock(http_request_parameters: HttpRequestParameters, object_uri: str) -> str:
6
+ response = request(
7
+ http_request_parameters=http_request_parameters,
8
+ uri=object_uri,
9
+ body="",
10
+ params={"_action": "LOCK", "accessMode": "MODIFY"},
11
+ method="POST",
12
+ )
13
+ if response.status_code == 200:
14
+ lock_handle = find_xml_element_text(response.text, ".//LOCK_HANDLE")
15
+ return lock_handle
16
+ else:
17
+ raise Exception(
18
+ f"{response.status_code} Failed to lock {object_uri}.\n{response.text}"
19
+ )
20
+
21
+
22
+ def unlock(
23
+ http_request_parameters: HttpRequestParameters, object_uri: str, lock_handle: str
24
+ ) -> bool:
25
+ response = request(
26
+ http_request_parameters=http_request_parameters,
27
+ uri=object_uri,
28
+ body="",
29
+ params={"_action": "UNLOCK", "lockHandle": lock_handle},
30
+ method="POST",
31
+ content_type="plain/text; charset=utf-8",
32
+ )
33
+ if response.status_code == 200:
34
+ return True
35
+ else:
36
+ raise Exception(
37
+ f"{response.status_code} Failed to unlock {object_uri}\n{response.text}"
38
+ )
@@ -0,0 +1,20 @@
1
+ from ..http_request import HttpRequestParameters, request
2
+
3
+
4
+ def login(http_request_parameters: HttpRequestParameters) -> str:
5
+
6
+ response = request(
7
+ http_request_parameters=http_request_parameters,
8
+ uri="/sap/bc/adt/compatibility/graph",
9
+ method="GET",
10
+ body="",
11
+ params={},
12
+ )
13
+
14
+ if response.status_code == 200:
15
+ csrf_token = response.headers.get("x-csrf-token")
16
+ if csrf_token is None:
17
+ raise Exception("CSRF token not found in response headers.")
18
+ return csrf_token
19
+ else:
20
+ raise Exception(f"{response.status_code} - Login failed.\n{response.text}")
@@ -0,0 +1,93 @@
1
+ from ..compat_typing import Literal, NotRequired, TypedDict
2
+ from ..http_request import HttpRequestParameters, request
3
+
4
+
5
+ class PrettyPrintSettings(TypedDict):
6
+ indentation: bool
7
+ style: Literal[
8
+ "toLower", "toUpper", "keywordLower", "keywordUpper", "none", "keywordAuto"
9
+ ]
10
+ condense_method_calls: NotRequired[bool]
11
+ default_auto_style: NotRequired[Literal["keywordLower", "keywordUpper"]]
12
+ format_method_calls: NotRequired[bool]
13
+ keep_identifier: NotRequired[bool]
14
+
15
+
16
+ def set_pretty_printer_settings(
17
+ http_request_parameters: HttpRequestParameters, settings: PrettyPrintSettings
18
+ ):
19
+
20
+ condense_method_calls = (
21
+ f"prettyprintersettings:condenseMethodCalls=\"{str(settings['condense_method_calls']).lower()}\""
22
+ if "condense_method_calls" in settings
23
+ else ""
24
+ )
25
+ default_auto_style = (
26
+ f"prettyprintersettings:defaultAutoStyle=\"{settings['default_auto_style']}\""
27
+ if "default_auto_style" in settings
28
+ else ""
29
+ )
30
+ format_method_calls = (
31
+ f"prettyprintersettings:formatMethodCalls=\"{str(settings['format_method_calls']).lower()}\""
32
+ if "format_method_calls" in settings
33
+ else ""
34
+ )
35
+ indentation = (
36
+ f"prettyprintersettings:indentation=\"{str(settings['indentation']).lower()}\""
37
+ if "indentation" in settings
38
+ else ""
39
+ )
40
+ keep_identifier = (
41
+ f"prettyprintersettings:keepIdentifier=\"{str(settings['keep_identifier']).lower()}\""
42
+ if "keep_identifier" in settings
43
+ else ""
44
+ )
45
+ style = (
46
+ f"prettyprintersettings:style=\"{settings['style']}\""
47
+ if "style" in settings
48
+ else ""
49
+ )
50
+
51
+ body = f"""<?xml version="1.0" encoding="UTF-8"?>
52
+ <prettyprintersettings:PrettyPrinterSettings
53
+ xmlns:prettyprintersettings="http://www.sap.com/adt/prettyprintersettings"
54
+ {condense_method_calls}
55
+ {default_auto_style}
56
+ {format_method_calls}
57
+ {indentation}
58
+ {keep_identifier}
59
+ {style}
60
+ />
61
+ """
62
+
63
+ response = request(
64
+ http_request_parameters,
65
+ uri="/sap/bc/adt/abapsource/prettyprinter/settings",
66
+ params={},
67
+ body=body,
68
+ method="PUT",
69
+ content_type="application/xml",
70
+ )
71
+ if response.status_code == 200:
72
+ return True
73
+ else:
74
+ raise Exception(
75
+ f"{response.status_code} Failed to set prettyprint settings\n{response.text}"
76
+ )
77
+
78
+
79
+ def prettyprint(http_request_parameters: HttpRequestParameters, src: str):
80
+ response = request(
81
+ http_request_parameters,
82
+ uri="/sap/bc/adt/abapsource/prettyprinter",
83
+ params={},
84
+ body=src,
85
+ method="POST",
86
+ content_type="text/plain; charset=utf-8",
87
+ )
88
+ if response.status_code == 200:
89
+ return response.text
90
+ else:
91
+ raise Exception(
92
+ f"{response.status_code} Failed to prettyprint\n{response.text}"
93
+ )
@@ -0,0 +1,18 @@
1
+ from ..http_request import HttpRequestParameters, request
2
+ from ..response_parsing import find_xml_elements_attributes
3
+ from ..compat_typing import List, Dict
4
+
5
+
6
+ def search_object(
7
+ http_request_parameters: HttpRequestParameters, query: str, max_results: int = 1
8
+ ) -> List[Dict[str, str]]:
9
+
10
+ response = request(
11
+ http_request_parameters=http_request_parameters,
12
+ uri="/sap/bc/adt/repository/informationsystem/search",
13
+ method="GET",
14
+ body="",
15
+ params={"operation": "quickSearch", "query": query, "maxResults": max_results},
16
+ )
17
+ elements = find_xml_elements_attributes(response.text, "adtcore:objectReference")
18
+ return elements
@@ -0,0 +1,101 @@
1
+ import xml.etree.ElementTree as et
2
+
3
+ from ..compat_typing import List, TypedDict
4
+ from ..api.xml_namespaces import XML_NAMESPACES
5
+ from ..http_request import HttpRequestParameters, request
6
+
7
+
8
+ class UnittestFlags:
9
+ def __init__(
10
+ self,
11
+ harmless: bool = True,
12
+ dangerous: bool = False,
13
+ critical: bool = False,
14
+ short: bool = True,
15
+ medium: bool = False,
16
+ long: bool = False,
17
+ ):
18
+ self.harmless = harmless
19
+ self.dangerous = dangerous
20
+ self.critical = critical
21
+ self.short = short
22
+ self.medium = medium
23
+ self.long = long
24
+
25
+
26
+ class UnitTestAlert(TypedDict):
27
+ title: str
28
+ kind: str
29
+ severity: str
30
+ details: List[str]
31
+
32
+
33
+ def _parse_alerts(xml_text: str) -> List[UnitTestAlert]:
34
+ root = et.fromstring(xml_text)
35
+ errors = []
36
+ alerts = root.findall(".//alert", XML_NAMESPACES)
37
+ for alert in alerts:
38
+ a = {}
39
+ title = alert.find(".//title", XML_NAMESPACES)
40
+ a["title"] = title.text if title is not None else ""
41
+ a["kind"] = alert.attrib.get("kind", "")
42
+ a["severity"] = alert.attrib.get("severity", "")
43
+ stack = alert.find(".//stack", XML_NAMESPACES)
44
+ a["stack"] = stack if stack is not None else []
45
+ details = []
46
+ for detail in alert.findall("./details/detail", XML_NAMESPACES):
47
+ detail_str = "\n".join(
48
+ x.attrib["text"]
49
+ for x in detail.iter()
50
+ if "text" in x.attrib is not None
51
+ )
52
+ details.append(detail_str)
53
+ a["details"] = details
54
+ errors.append(a)
55
+ return errors
56
+
57
+
58
+ def run_unit_test(
59
+ http_request_parameters: HttpRequestParameters,
60
+ object_uri: str,
61
+ unit_test_flags: UnittestFlags = UnittestFlags(),
62
+ ):
63
+ body = f"""
64
+ <?xml version="1.0" encoding="UTF-8"?>
65
+ <aunit:runConfiguration xmlns:aunit="http://www.sap.com/adt/aunit">
66
+ <external>
67
+ <coverage active="false"/>
68
+ </external>
69
+ <options>
70
+ <uriType value="semantic"/>
71
+ <testDeterminationStrategy sameProgram="true" assignedTests="false"/>
72
+ <testRiskLevels harmless="{str(unit_test_flags.harmless).lower()}" dangerous="{str(unit_test_flags.dangerous).lower()}" critical="{str(unit_test_flags.critical).lower()}"/>
73
+ <testDurations short="{str(unit_test_flags.short).lower()}" medium="{str(unit_test_flags.medium).lower()}" long="{str(unit_test_flags.long).lower()}"/>
74
+ <withNavigationUri enabled="true"/>
75
+ </options>
76
+ <adtcore:objectSets xmlns:adtcore="http://www.sap.com/adt/core">
77
+ <objectSet kind="inclusive">
78
+ <adtcore:objectReferences>
79
+ <adtcore:objectReference adtcore:uri="{object_uri}"/>
80
+ </adtcore:objectReferences>
81
+ </objectSet>
82
+ </adtcore:objectSets>
83
+ </aunit:runConfiguration>
84
+ """
85
+
86
+ response = request(
87
+ http_request_parameters=http_request_parameters,
88
+ uri="/sap/bc/adt/abapunit/testruns",
89
+ method="POST",
90
+ body=body,
91
+ params={},
92
+ content_type="application/xml",
93
+ )
94
+ if response.status_code == 200:
95
+ alerts = _parse_alerts(response.text)
96
+ return alerts
97
+
98
+ else:
99
+ raise Exception(
100
+ f"{response.status_code} - Failed to run unit test for {object_uri}\n{response.text}"
101
+ )
@@ -0,0 +1,8 @@
1
+ XML_NAMESPACES = {
2
+ "chkl": "http://www.sap.com/abapxml/checklist",
3
+ "atom": "http://www.w3.org/2005/Atom",
4
+ "adtcore": "http://www.sap.com/adt/core",
5
+ "exc": "http://www.sap.com/abapxml/types/communicationframework",
6
+ "asx": "http://www.sap.com/abapxml",
7
+ "aunit": "http://www.sap.com/adt/aunit",
8
+ }
@@ -0,0 +1,14 @@
1
+ try:
2
+ from typing import Literal, NotRequired, TypeAlias, Optional, Dict, List, TypedDict # Python 3.11+
3
+ except ImportError:
4
+ try:
5
+ from typing import Literal # Python 3.8 - 3.10
6
+ except ImportError:
7
+ from typing_extensions import Literal # Python 3.7 fallback
8
+
9
+ from typing_extensions import NotRequired
10
+ from typing_extensions import TypeAlias
11
+ from typing_extensions import Optional
12
+ from typing_extensions import Dict
13
+ from typing_extensions import List
14
+ from typing_extensions import TypedDict
@@ -0,0 +1,46 @@
1
+ import requests
2
+ from .compat_typing import Literal, TypedDict
3
+
4
+
5
+ class HttpRequestParameters(TypedDict):
6
+ host: str
7
+ csrf_token: str
8
+ statefulness: Literal["stateless", "stateful"]
9
+ request_number: int
10
+ session: requests.Session
11
+
12
+
13
+ def request(
14
+ http_request_parameters: HttpRequestParameters,
15
+ uri: str,
16
+ method: Literal["GET", "POST", "PUT", "DELETE"],
17
+ body: str,
18
+ params: dict,
19
+ content_type: str = "application/xml",
20
+ ) -> requests.Response:
21
+
22
+ config = {
23
+ "params": params,
24
+ "headers": {
25
+ "Accept": "*/*",
26
+ "Cache-Control": "no-cache",
27
+ "x-csrf-token": http_request_parameters["csrf_token"],
28
+ "X-sap-adt-sessiontype": http_request_parameters["statefulness"],
29
+ "content-type": content_type,
30
+ },
31
+ "url": http_request_parameters["host"] + uri,
32
+ "data": body,
33
+ }
34
+
35
+ session = http_request_parameters["session"]
36
+ if method == "POST":
37
+ response = session.post(**config)
38
+ elif method == "GET":
39
+ response = session.get(**config)
40
+ elif method == "PUT":
41
+ response = session.put(**config)
42
+ elif method == "DELETE":
43
+ response = session.delete(**config)
44
+ else:
45
+ raise ValueError(f"Unsupported method: {method}")
46
+ return response
@@ -0,0 +1,43 @@
1
+ from xml.etree import ElementTree as et
2
+ from .compat_typing import Optional, Dict, List
3
+ from .api.xml_namespaces import XML_NAMESPACES
4
+
5
+
6
+ def _strip_namespace(name: str) -> str:
7
+ if name.startswith("{"):
8
+ return name.split("}", 1)[1]
9
+ if ":" in name:
10
+ return name.split(":", 1)[1]
11
+ return name
12
+
13
+
14
+ def _et_to_attributes_dict(element: Optional[et.Element]) -> Dict[str, str]:
15
+ cleaned = (
16
+ {_strip_namespace(key): value for key, value in element.attrib.items()}
17
+ if element is not None
18
+ else {}
19
+ )
20
+ return cleaned
21
+
22
+
23
+ def find_xml_elements_attributes(xml_text: str, tag_name: str) -> List[Dict[str, str]]:
24
+ root = et.fromstring(xml_text)
25
+ elements = root.findall(tag_name, XML_NAMESPACES)
26
+ processed_elements = []
27
+ for element in elements:
28
+ cleaned = _et_to_attributes_dict(element)
29
+ processed_elements.append(cleaned)
30
+ return processed_elements
31
+
32
+
33
+ def find_xml_element_attributes(xml_text: str, tag_name: str) -> Dict[str, str]:
34
+ root = et.fromstring(xml_text)
35
+ element = root.find(tag_name, XML_NAMESPACES)
36
+ element = _et_to_attributes_dict(element)
37
+ return element
38
+
39
+
40
+ def find_xml_element_text(xml_text: str, tag_name: str):
41
+ root = et.fromstring(xml_text)
42
+ el = root.find(tag_name, XML_NAMESPACES)
43
+ return "".join(el.itertext()).strip() if el is not None else ""
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: abap-adt-py
3
+ Version: 0.1.0
4
+ Summary: A Python library for interacting with SAP systems using the ADT (ABAP Development Tools) protocol.
5
+ Author-email: Tim Köhne <20153452+timkoehne@users.noreply.github.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2023 timkoehne
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Project-URL: Homepage, https://github.com/timkoehne/abap-adt-py
28
+ Project-URL: Bug Tracker, https://github.com/timkoehne/abap-adt-py/issues
29
+ Classifier: Programming Language :: Python :: 3
30
+ Classifier: Programming Language :: Python :: 3.7
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Operating System :: OS Independent
33
+ Requires-Python: >=3.7
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENSE
36
+ Requires-Dist: requests>=2.25
37
+ Requires-Dist: typing-extensions>=4.7.1
38
+ Dynamic: license-file
@@ -0,0 +1,24 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/abap_adt_py/__init__.py
5
+ src/abap_adt_py/adt_client.py
6
+ src/abap_adt_py/compat_typing.py
7
+ src/abap_adt_py/http_request.py
8
+ src/abap_adt_py/response_parsing.py
9
+ src/abap_adt_py.egg-info/PKG-INFO
10
+ src/abap_adt_py.egg-info/SOURCES.txt
11
+ src/abap_adt_py.egg-info/dependency_links.txt
12
+ src/abap_adt_py.egg-info/requires.txt
13
+ src/abap_adt_py.egg-info/top_level.txt
14
+ src/abap_adt_py/api/__init__.py
15
+ src/abap_adt_py/api/activate.py
16
+ src/abap_adt_py/api/content.py
17
+ src/abap_adt_py/api/create.py
18
+ src/abap_adt_py/api/delete.py
19
+ src/abap_adt_py/api/lock.py
20
+ src/abap_adt_py/api/login.py
21
+ src/abap_adt_py/api/prettyprint.py
22
+ src/abap_adt_py/api/search.py
23
+ src/abap_adt_py/api/unittest.py
24
+ src/abap_adt_py/api/xml_namespaces.py
@@ -0,0 +1,2 @@
1
+ requests>=2.25
2
+ typing-extensions>=4.7.1
@@ -0,0 +1 @@
1
+ abap_adt_py