asmb6-api 1.0.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,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: asmb6-api
3
+ Version: 1.0.0
4
+ Summary: Python API for ASMB6-iKVM remote management.
5
+ Home-page: https://github.com/grifonice99/asmb6-api
6
+ Author: Grifonice99
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.7,<4
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: requests<3,>=2.28
12
+ Dynamic: author
13
+ Dynamic: classifier
14
+ Dynamic: description
15
+ Dynamic: description-content-type
16
+ Dynamic: home-page
17
+ Dynamic: requires-dist
18
+ Dynamic: requires-python
19
+ Dynamic: summary
20
+
21
+ # ASMB6 API
22
+
23
+ A Python interface for interacting with the ASMB6-iKVM remote management module.
24
+
25
+ This library provides programmatic access to authentication, session handling, hardware monitoring, and basic power management functions through the module’s HTTP management interface.
26
+
27
+ ---
28
+
29
+ ## Overview
30
+
31
+ The ASMB6 API is designed as a lightweight Python wrapper around the module’s web-based management interface. It enables automation and integration into scripts or monitoring systems without relying on the WebUI.
32
+
33
+ The library provides:
34
+
35
+ * Session-based authentication
36
+ * Structured sensor objects
37
+ * Power and host state queries
38
+ * Programmatic power control operations
39
+
40
+ The goal is to expose a clean and predictable Python interface suitable for automation and monitoring tasks.
41
+
42
+ ---
43
+
44
+ ## Features
45
+
46
+ ### Authentication & Session Management
47
+
48
+ * Login and logout support
49
+ * Session validation
50
+ * Explicit return codes for API calls
51
+
52
+ ### Hardware Monitoring
53
+
54
+ * Temperature sensors
55
+ * Voltage sensors
56
+ * Fan speed monitoring
57
+ * Power supply status
58
+ * Structured `Sensors` container with parsed values
59
+ * Access to raw sensor payload for debugging
60
+
61
+ ### Host Control & Status
62
+
63
+ * Retrieve host power state
64
+ * Query power button status
65
+ * Power control operations (shutdown, power on, reset, cycle)
66
+
67
+ ---
68
+
69
+ ## Installation
70
+
71
+ Clone the repository:
72
+
73
+ ```bash
74
+ git clone https://github.com/Grifonice99/asmb6-api
75
+ cd asmb6-api
76
+ ```
77
+
78
+ Install required dependency:
79
+
80
+ ```bash
81
+ pip install requests
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Basic Usage
87
+
88
+ ```python
89
+ from apmi import APMI
90
+
91
+ apmi = APMI("192.168.1.100")
92
+
93
+ # Authenticate
94
+ status = apmi.login("admin", "password")
95
+ if status != 0:
96
+ raise RuntimeError(f"Login failed with code {status}")
97
+
98
+ # Query host power state
99
+ powered_on = apmi.status()
100
+ print("Host is on" if powered_on else "Host is off")
101
+
102
+ # Retrieve sensors
103
+ sensors = apmi.sensors()
104
+ print(sensors.cpu1.reading)
105
+
106
+ # Logout
107
+ apmi.logout()
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Design Notes
113
+
114
+ * Command execution methods return explicit status codes.
115
+ * Device state queries return domain-level values (e.g., `bool`).
116
+ * Sensor data is exposed through structured objects rather than raw dictionaries.
117
+ * Raw sensor payload data remains accessible for debugging or inspection purposes.
118
+
119
+ ---
120
+
121
+ ## Disclaimer
122
+
123
+ This project was developed and tested on an ASUS Z9PE-D16 motherboard equipped with an ASMB6-iKVM module.
124
+
125
+ Compatibility with other motherboards, firmware versions, or ASMB6 variants is not guaranteed. Behavior may differ depending on firmware revisions or platform-specific implementations.
126
+
127
+ Users are encouraged to test carefully in their own environment before relying on this library in production systems.
128
+
129
+ ---
130
+
131
+ ## License
132
+
133
+ This project is licensed under the terms defined in the `LICENSE` file.
@@ -0,0 +1,3 @@
1
+ from .apmi_interface import APMI
2
+ from .parser import Sensors
3
+ __version__ = "1.0.0"
@@ -0,0 +1,161 @@
1
+ import requests
2
+ import re
3
+ import json
4
+ from .parser import sanitize, serialize, parse_sensors, Sensors
5
+
6
+ start_ref = """<?xml version="1.0" encoding="UTF-8"?>
7
+
8
+ <jnlp spec="1.0+" codebase="""
9
+ end_ref = """ </application-desc>
10
+ </jnlp>
11
+ """
12
+
13
+
14
+ class APMI:
15
+ def __init__(self, ip, http_port=80):
16
+ self.http_port = http_port
17
+ self.username = None
18
+ self.ip = ip
19
+ self.session = requests.Session()
20
+ self.base = f"http://{self.ip}:{self.http_port}"
21
+
22
+ def login(self, username, password) -> int:
23
+ """
24
+ Important:
25
+ Always call logout() before terminating the program.
26
+
27
+ Failing to do so may leave an active session on the
28
+ ASMB6-iKVM, which can cause login issues during
29
+ development or repeated test runs.
30
+ """
31
+ data = {
32
+ "WEBVAR_USERNAME": username,
33
+ "WEBVAR_PASSWORD": password,
34
+ }
35
+
36
+ res = self.session.post(self.base + "/rpc/WEBSES/create.asp", data=data)
37
+
38
+ json_res = json.loads(serialize(sanitize(res.text)))["WEBVAR_JSONVAR_WEB_SESSION"]
39
+ status = json_res["HAPI_STATUS"]
40
+ if status != 0:
41
+ print(f"Invalid credential")
42
+ else:
43
+ session_cookie = json_res["WEBVAR_STRUCTNAME_WEB_SESSION"][0]["SESSION_COOKIE"]
44
+
45
+ self.session.cookies.set("Username", username, domain=self.ip, path="/")
46
+ self.session.cookies.set("Language", "EN", domain=self.ip, path="/")
47
+ self.session.cookies.set("SessionCookie", session_cookie, domain=self.ip, path="/")
48
+ self.username = username
49
+
50
+ return status
51
+
52
+ def logout(self)->int:
53
+ res = self.session.get(self.base + "/rpc/WEBSES/logout.asp")
54
+ self.session = requests.Session()
55
+ self.username = None
56
+ return json.loads(serialize(sanitize(res.text)))["WEBVAR_JSONVAR_WEB_SESSION_LOGOUT"]["HAPI_STATUS"]
57
+
58
+ def validate_session(self)->int:
59
+ if not self.username:
60
+ raise Exception("You need to be authenticated")
61
+ res = self.session.get(self.base + "/rpc/WEBSES/validate.asp").text
62
+ return json.loads(serialize(sanitize(res.text)))["WEBVAR_JSONVAR_WEB_SESSION_VALIDATE"]["HAPI_STATUS"]
63
+
64
+ def sensors(self) -> Sensors:
65
+ if not self.username:
66
+ raise Exception("You need to be authenticated")
67
+ res = self.session.get(self.base + "/rpc/getallsensors.asp")
68
+ raw = json.loads(serialize(sanitize(res.text)))
69
+ sensors_data = raw["WEBVAR_JSONVAR_HL_GETALLSENSORS"][
70
+ "WEBVAR_STRUCTNAME_HL_GETALLSENSORS"
71
+ ]
72
+ sensors = parse_sensors(sensors_data)
73
+ return sensors
74
+
75
+ def status(self) -> int:
76
+ if not self.username:
77
+ raise Exception("You need to be authenticated")
78
+ res = self.session.get(self.base + "/rpc/hoststatus.asp")
79
+ raw = json.loads(serialize(sanitize(res.text)))
80
+ return raw["WEBVAR_JSONVAR_HL_SYSTEM_STATE"][
81
+ "WEBVAR_STRUCTNAME_HL_SYSTEM_STATE"
82
+ ][0]["JF_STATE"]
83
+
84
+ def pwr_btn_status(self) -> int:
85
+ if not self.username:
86
+ raise Exception("You need to be authenticated")
87
+ res = self.session.get(self.base + "/rpc/PWRbutton.asp")
88
+ raw = json.loads(serialize(sanitize(res.text)))
89
+ return raw["WEBVAR_JSONVAR_HL_BUTTON_STATE"][
90
+ "WEBVAR_STRUCTNAME_HL_BUTTON_STATE"
91
+ ][0]["PB_STATE"]
92
+
93
+ def turn_off_board(self, immediate=False) -> None:
94
+ """
95
+ Note:
96
+ immediate=True may not turn the board off if it was powered on
97
+ less than 15 seconds before this call.
98
+ """
99
+ if not self.username:
100
+ raise Exception("You need to be authenticated")
101
+ if immediate:
102
+ res = self.session.post(
103
+ self.base + "/rpc/hostctl.asp", data={"WEBVAR_POWER_CMD": 0}
104
+ )
105
+ else:
106
+ res = self.session.post(
107
+ self.base + "/rpc/hostctl.asp", data={"WEBVAR_POWER_CMD": 5}
108
+ )
109
+
110
+ def turn_on_board(self) -> None:
111
+ if not self.username:
112
+ raise Exception("You need to be authenticated")
113
+ res = self.session.post(
114
+ self.base + "/rpc/hostctl.asp", data={"WEBVAR_POWER_CMD": 1}
115
+ )
116
+
117
+ def power_cycle_board(self) -> None:
118
+ if not self.username:
119
+ raise Exception("You need to be authenticated")
120
+ res = self.session.post(
121
+ self.base + "/rpc/hostctl.asp", data={"WEBVAR_POWER_CMD": 2}
122
+ )
123
+
124
+ def reset_board(self) -> None:
125
+ if not self.username:
126
+ raise Exception("You need to be authenticated")
127
+ res = self.session.post(
128
+ self.base + "/rpc/hostctl.asp", data={"WEBVAR_POWER_CMD": 3}
129
+ )
130
+
131
+ def get_events(self) -> dict:
132
+ if not self.username:
133
+ raise Exception("You need to be authenticated")
134
+ res = self.session.get(self.base + "/rpc/getallselentries.asp")
135
+ result = json.loads(serialize(sanitize(res.text)))[
136
+ "WEBVAR_JSONVAR_HL_GETALLSELENTRIES"
137
+ ]["WEBVAR_STRUCTNAME_HL_GETALLSELENTRIES"][:-1]
138
+ return result
139
+
140
+ def get_jnlp(self) -> str:
141
+ if not self.username:
142
+ raise Exception("You need to be authenticated")
143
+
144
+ res = self.session.get(
145
+ self.base + f"/Java/jviewer.jnlp?EXTRNIP={self.base}&JNLPSTR=JViewer",
146
+ stream=True,
147
+ )
148
+
149
+ data = b""
150
+ try:
151
+ for chunk in res.iter_content(1):
152
+ if chunk:
153
+ data += chunk
154
+ except requests.exceptions.RequestException as e:
155
+ # Strings parsed from the firmware's jviewer.jnlp template.
156
+ if data.startswith(start_ref.encode()) and data.endswith(end_ref.encode()):
157
+ print("Notice: The data appears to have downloaded correctly, but the firmware closed the connection before all content-length data was received.")
158
+ else:
159
+ raise e
160
+
161
+ return data.decode()
@@ -0,0 +1,167 @@
1
+ import re
2
+
3
+ # IPMI sensor type codes:
4
+ # 1 : Temperature
5
+ # 2 : Voltage
6
+ # 4 : Fan
7
+ # 5 : Physical Security
8
+ # 8 : PMB Power
9
+ # 12 : Memory ECC Sensor
10
+ # 13 : Drive Slot
11
+ # 35 : Watchdog
12
+ # 193 : OEM Memory ECC Sensor
13
+ # 220 : Node Manager capabilities
14
+
15
+
16
+ class base:
17
+ def __init__(self):
18
+ super().__init__()
19
+ self.reading = None
20
+ self.unr = None
21
+ self.uc = None
22
+ self.unc = None
23
+ self.lnr = None
24
+ self.lc = None
25
+ self.lnc = None
26
+ self.state = None
27
+
28
+
29
+ class Temperature(base):
30
+ def __init__(self):
31
+ super().__init__()
32
+
33
+
34
+ class Voltage(base):
35
+
36
+ def __init__(self):
37
+ super().__init__()
38
+
39
+
40
+ class Fan(base):
41
+ def __init__(self):
42
+ super().__init__()
43
+
44
+
45
+ class StateOnly:
46
+ def __init__(self):
47
+ self.state = None
48
+
49
+
50
+ class PowerSupply(base):
51
+ def __init__(self):
52
+ super().__init__()
53
+
54
+
55
+ class Fan(base):
56
+ def __init__(self):
57
+ super().__init__()
58
+
59
+
60
+ class Sensors:
61
+ def __init__(self):
62
+ self.cpu1: Temperature = Temperature()
63
+ self.cpu2: Temperature = Temperature()
64
+ self.tr1: Temperature = Temperature()
65
+ self.tr2: Temperature = Temperature()
66
+ self.vcore1: Voltage = Voltage()
67
+ self.vcore2: Voltage = Voltage()
68
+ self.vtt_cpu: Voltage = Voltage()
69
+ self.vddq_ab_cpu1: Voltage = Voltage()
70
+ self.vddq_cd_cpu1: Voltage = Voltage()
71
+ self.vddq_ef_cpu2: Voltage = Voltage()
72
+ self.vddq_gh_cpu2: Voltage = Voltage()
73
+ self.v12: Voltage = Voltage()
74
+ self.v5: Voltage = Voltage()
75
+ self.vsb5: Voltage = Voltage()
76
+ self.v3_3: Voltage = Voltage()
77
+ self.vsb3_3: Voltage = Voltage()
78
+ self.vbat: Voltage = Voltage()
79
+ self.cpu_fan1: Fan = Fan()
80
+ self.cpu_fan2: Fan = Fan()
81
+ self.frnt_fan1: Fan = Fan()
82
+ self.frnt_fan2: Fan = Fan()
83
+ self.frnt_fan3: Fan = Fan()
84
+ self.frnt_fan4: Fan = Fan()
85
+ self.rear_fan1: Fan = Fan()
86
+ self.rear_fan2: Fan = Fan()
87
+ self.cpu1_ecc1: StateOnly = StateOnly()
88
+ self.cpu1_ecc2: StateOnly = StateOnly()
89
+ self.cpu2_ecc1: StateOnly = StateOnly()
90
+ self.cpu2_ecc2: StateOnly = StateOnly()
91
+ self.pmbpower: PowerSupply = PowerSupply()
92
+ self.chassisintrusion: StateOnly = StateOnly()
93
+ self.watchdog2: StateOnly = StateOnly()
94
+ self.nm_capabilities: StateOnly = StateOnly()
95
+ self.raw = {}
96
+
97
+
98
+ def serialize(raw):
99
+ raw = raw.replace("=", ":")
100
+ raw = raw.replace(";", "")
101
+ raw = re.sub(r"(\b[A-Za-z_][A-Za-z0-9_]*\b)\s*:", r'"\1":', raw)
102
+ raw = raw.replace("'", '"')
103
+ return raw
104
+
105
+
106
+ def sanitize(text: str):
107
+ return (
108
+ "{"
109
+ + "\n".join(filter(lambda x: not x.startswith("//"), text.split("\n")))
110
+ + "}"
111
+ )
112
+
113
+
114
+ def Parse_data(raw) -> base:
115
+ sensor: base = base()
116
+ sensor.lnc = raw["LowNCThresh"]
117
+ sensor.lc = raw["LowCTThresh"]
118
+ sensor.lnr = raw["LowNRThresh"]
119
+ sensor.unc = raw["HighNCThresh"]
120
+ sensor.uc = raw["HighCTThresh"]
121
+ sensor.unr = raw["HighNRThresh"]
122
+ sensor.reading = raw["SensorReading"]/1000
123
+ sensor.state = raw["SensorState"]
124
+ return sensor
125
+
126
+
127
+ def parse_sensors(raw) -> Sensors:
128
+ SensorsDict = {}
129
+ for x in raw:
130
+ if "SensorNumber" in x:
131
+ SensorsDict[x["SensorNumber"]] = x
132
+
133
+ sensors = Sensors()
134
+ sensors.cpu1 = Parse_data(SensorsDict[49])
135
+ sensors.cpu2 = Parse_data(SensorsDict[50])
136
+ sensors.vcore1 = Parse_data(SensorsDict[52])
137
+ sensors.vcore2 = Parse_data(SensorsDict[53])
138
+ sensors.v3_3 = Parse_data(SensorsDict[54])
139
+ sensors.v5 = Parse_data(SensorsDict[55])
140
+ sensors.v12 = Parse_data(SensorsDict[56])
141
+ sensors.vsb5 = Parse_data(SensorsDict[59])
142
+ sensors.vbat = Parse_data(SensorsDict[60])
143
+ sensors.vtt_cpu = Parse_data(SensorsDict[61])
144
+ sensors.vsb3_3 = Parse_data(SensorsDict[64])
145
+ sensors.vddq_ab_cpu1 = Parse_data(SensorsDict[77])
146
+ sensors.vddq_cd_cpu1 = Parse_data(SensorsDict[78])
147
+ sensors.vddq_ef_cpu2 = Parse_data(SensorsDict[80])
148
+ sensors.vddq_gh_cpu2 = Parse_data(SensorsDict[81])
149
+ sensors.cpu_fan1 = Parse_data(SensorsDict[160])
150
+ sensors.cpu_fan2 = Parse_data(SensorsDict[161])
151
+ sensors.frnt_fan1 = Parse_data(SensorsDict[162])
152
+ sensors.frnt_fan2 = Parse_data(SensorsDict[163])
153
+ sensors.frnt_fan3 = Parse_data(SensorsDict[164])
154
+ sensors.frnt_fan4 = Parse_data(SensorsDict[165])
155
+ sensors.rear_fan1 = Parse_data(SensorsDict[166])
156
+ sensors.rear_fan2 = Parse_data(SensorsDict[167])
157
+ sensors.tr1 = Parse_data(SensorsDict[204])
158
+ sensors.tr2 = Parse_data(SensorsDict[205])
159
+ sensors.nm_capabilities.state = SensorsDict[26]["SensorState"]
160
+ sensors.chassisintrusion.state = SensorsDict[79]["SensorState"]
161
+ sensors.cpu1_ecc1.state = SensorsDict[209]["SensorState"]
162
+ sensors.cpu1_ecc2.state = SensorsDict[210]["SensorState"]
163
+ sensors.cpu2_ecc1.state = SensorsDict[211]["SensorState"]
164
+ sensors.cpu2_ecc2.state = SensorsDict[212]["SensorState"]
165
+ sensors.watchdog2.state = SensorsDict[255]["SensorState"]
166
+ sensors.raw=SensorsDict
167
+ return sensors
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: asmb6-api
3
+ Version: 1.0.0
4
+ Summary: Python API for ASMB6-iKVM remote management.
5
+ Home-page: https://github.com/grifonice99/asmb6-api
6
+ Author: Grifonice99
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.7,<4
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: requests<3,>=2.28
12
+ Dynamic: author
13
+ Dynamic: classifier
14
+ Dynamic: description
15
+ Dynamic: description-content-type
16
+ Dynamic: home-page
17
+ Dynamic: requires-dist
18
+ Dynamic: requires-python
19
+ Dynamic: summary
20
+
21
+ # ASMB6 API
22
+
23
+ A Python interface for interacting with the ASMB6-iKVM remote management module.
24
+
25
+ This library provides programmatic access to authentication, session handling, hardware monitoring, and basic power management functions through the module’s HTTP management interface.
26
+
27
+ ---
28
+
29
+ ## Overview
30
+
31
+ The ASMB6 API is designed as a lightweight Python wrapper around the module’s web-based management interface. It enables automation and integration into scripts or monitoring systems without relying on the WebUI.
32
+
33
+ The library provides:
34
+
35
+ * Session-based authentication
36
+ * Structured sensor objects
37
+ * Power and host state queries
38
+ * Programmatic power control operations
39
+
40
+ The goal is to expose a clean and predictable Python interface suitable for automation and monitoring tasks.
41
+
42
+ ---
43
+
44
+ ## Features
45
+
46
+ ### Authentication & Session Management
47
+
48
+ * Login and logout support
49
+ * Session validation
50
+ * Explicit return codes for API calls
51
+
52
+ ### Hardware Monitoring
53
+
54
+ * Temperature sensors
55
+ * Voltage sensors
56
+ * Fan speed monitoring
57
+ * Power supply status
58
+ * Structured `Sensors` container with parsed values
59
+ * Access to raw sensor payload for debugging
60
+
61
+ ### Host Control & Status
62
+
63
+ * Retrieve host power state
64
+ * Query power button status
65
+ * Power control operations (shutdown, power on, reset, cycle)
66
+
67
+ ---
68
+
69
+ ## Installation
70
+
71
+ Clone the repository:
72
+
73
+ ```bash
74
+ git clone https://github.com/Grifonice99/asmb6-api
75
+ cd asmb6-api
76
+ ```
77
+
78
+ Install required dependency:
79
+
80
+ ```bash
81
+ pip install requests
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Basic Usage
87
+
88
+ ```python
89
+ from apmi import APMI
90
+
91
+ apmi = APMI("192.168.1.100")
92
+
93
+ # Authenticate
94
+ status = apmi.login("admin", "password")
95
+ if status != 0:
96
+ raise RuntimeError(f"Login failed with code {status}")
97
+
98
+ # Query host power state
99
+ powered_on = apmi.status()
100
+ print("Host is on" if powered_on else "Host is off")
101
+
102
+ # Retrieve sensors
103
+ sensors = apmi.sensors()
104
+ print(sensors.cpu1.reading)
105
+
106
+ # Logout
107
+ apmi.logout()
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Design Notes
113
+
114
+ * Command execution methods return explicit status codes.
115
+ * Device state queries return domain-level values (e.g., `bool`).
116
+ * Sensor data is exposed through structured objects rather than raw dictionaries.
117
+ * Raw sensor payload data remains accessible for debugging or inspection purposes.
118
+
119
+ ---
120
+
121
+ ## Disclaimer
122
+
123
+ This project was developed and tested on an ASUS Z9PE-D16 motherboard equipped with an ASMB6-iKVM module.
124
+
125
+ Compatibility with other motherboards, firmware versions, or ASMB6 variants is not guaranteed. Behavior may differ depending on firmware revisions or platform-specific implementations.
126
+
127
+ Users are encouraged to test carefully in their own environment before relying on this library in production systems.
128
+
129
+ ---
130
+
131
+ ## License
132
+
133
+ This project is licensed under the terms defined in the `LICENSE` file.
@@ -0,0 +1,9 @@
1
+ setup.py
2
+ asmb6/__init__.py
3
+ asmb6/apmi_interface.py
4
+ asmb6/parser.py
5
+ asmb6_api.egg-info/PKG-INFO
6
+ asmb6_api.egg-info/SOURCES.txt
7
+ asmb6_api.egg-info/dependency_links.txt
8
+ asmb6_api.egg-info/requires.txt
9
+ asmb6_api.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ requests<3,>=2.28
@@ -0,0 +1 @@
1
+ asmb6
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,33 @@
1
+ from setuptools import setup, find_packages
2
+ from pathlib import Path
3
+ import re
4
+
5
+ here = Path(__file__).parent.resolve()
6
+
7
+ init_text = (here / "asmb6" / "__init__.py").read_text(encoding="utf-8")
8
+ match = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']',
9
+ init_text, re.M)
10
+
11
+ if not match:
12
+ raise RuntimeError("Version not found")
13
+
14
+ version = match.group(1)
15
+
16
+ long_description = (here / "readme.md").read_text(encoding="utf-8")
17
+
18
+ setup(
19
+ name="asmb6-api",
20
+ version=version,
21
+ description="Python API for ASMB6-iKVM remote management.",
22
+ long_description=long_description,
23
+ long_description_content_type="text/markdown",
24
+ url="https://github.com/grifonice99/asmb6-api",
25
+ author="Grifonice99",
26
+ python_requires=">=3.7,<4",
27
+ packages=find_packages(exclude=("tests", "docs")),
28
+ install_requires=["requests>=2.28,<3"],
29
+ classifiers=[
30
+ "Programming Language :: Python :: 3",
31
+ "Operating System :: OS Independent",
32
+ ],
33
+ )