mirror of
https://github.com/kellyjonbrazil/jc.git
synced 2025-06-15 00:05:11 +02:00
created the amixer sget command parser - READY FOR REVIEW (#616)
* created the amixer first skeleton
* push testing and integrate this commit and branch with issue: #591
* #591 checks the input data with jc utils
* created the data parser of the sget control of the amixer sget <controller> command.
* test commit - just for tests
* another test commit
* another test commit
* created a dedicated pseudo algorithm for the amixer sget and tried various of strings.
* orginized the docstring with general explanation about the tool and the amixer tool output and algorithm of the input parsing and input examples.
* created raw implementation, but it's raw either or either.
* orginized the content inside the amixer parser
* removed endpoint name
* added amixer to the jc parser in lib
* more explanations
* added tests for the amixer sget
* added tests for the amixer sget
* fine versioning fix
* created docstring+another explanations seperated.
* created the amixer parser docu
* added the amixer in alphabet order to the json convert lib
* Fix PEP 8: E302 violation as part of boy scout principle
* deleted not necessary file
* fixed the spaces between sections in the amixer description
* resolved commits such as amixer module docstring and preperations for parser for raw=False.
* Revert "Fix PEP 8: E302 violation as part of boy scout principle"
This reverts commit 241d1a1c63
.
* created the dedicated _process for raw=False
* created the dedicated _process for raw=False
* added tests for the _process raw=False.
* changed keys to be lowercase snake-case - Change 'dB' to 'db'
* added more dB -> db changes and used int convertor of the jc utils
---------
Co-authored-by: EdenRafael <eden.refael@kazuar.com>
Co-authored-by: Eden Refael <edeenraf@hotmail.com>
Co-authored-by: Kelly Brazil <kellyjonbrazil@gmail.com>
This commit is contained in:
@ -16,6 +16,7 @@ parsers: List[str] = [
|
||||
'acpi',
|
||||
'airport',
|
||||
'airport-s',
|
||||
'amixer',
|
||||
'apt-cache-show',
|
||||
'apt-get-sqq',
|
||||
'arp',
|
||||
|
277
jc/parsers/amixer.py
Normal file
277
jc/parsers/amixer.py
Normal file
@ -0,0 +1,277 @@
|
||||
r"""jc - JSON Convert `amixer sget` command output parser
|
||||
|
||||
Usage (cli):
|
||||
|
||||
$ amixer sget <control_name> | jc --amixer
|
||||
$ amixer sget Master | jc --amixer
|
||||
$ amixer sget Capture | jc --amixer
|
||||
$ amixer sget Speakers | jc --amixer
|
||||
|
||||
Usage (module):
|
||||
|
||||
import jc
|
||||
result = jc.parse('amixer', <amixer sget command output>)
|
||||
|
||||
Schema:
|
||||
|
||||
{
|
||||
"control_name": string,
|
||||
"capabilities": [
|
||||
string
|
||||
],
|
||||
"playback_channels": [
|
||||
string
|
||||
],
|
||||
"limits": {
|
||||
"playback_min": integer,
|
||||
"playback_max": integer
|
||||
},
|
||||
"mono": {
|
||||
"playback_value": integer,
|
||||
"percentage": integer,
|
||||
"db": float,
|
||||
"status": boolean
|
||||
}
|
||||
}
|
||||
|
||||
Examples:
|
||||
|
||||
$ amixer sget Master | jc --amixer -p
|
||||
{
|
||||
"control_name": "Capture",
|
||||
"capabilities": [
|
||||
"cvolume",
|
||||
"cswitch"
|
||||
],
|
||||
"playback_channels": [],
|
||||
"limits": {
|
||||
"playback_min": 0,
|
||||
"playback_max": 63
|
||||
},
|
||||
"front_left": {
|
||||
"playback_value": 63,
|
||||
"percentage": 100,
|
||||
"db": 30.0,
|
||||
"status": true
|
||||
},
|
||||
"front_right": {
|
||||
"playback_value": 63,
|
||||
"percentage": 100,
|
||||
"db": 30.0,
|
||||
"status": true
|
||||
}
|
||||
}
|
||||
|
||||
$ amixer sget Master | jc --amixer -p -r
|
||||
{
|
||||
"control_name": "Master",
|
||||
"capabilities": [
|
||||
"pvolume",
|
||||
"pvolume-joined",
|
||||
"pswitch",
|
||||
"pswitch-joined"
|
||||
],
|
||||
"playback_channels": [
|
||||
"Mono"
|
||||
],
|
||||
"limits": {
|
||||
"playback_min": "0",
|
||||
"playback_max": "87"
|
||||
},
|
||||
"mono": {
|
||||
"playback_value": "87",
|
||||
"percentage": "100%",
|
||||
"db": "0.00db",
|
||||
"status": "on"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
"""
|
||||
from typing import List, Dict
|
||||
|
||||
import jc.utils
|
||||
from jc.utils import convert_to_int
|
||||
|
||||
class info():
|
||||
"""Provides parser metadata (version, author, etc.)"""
|
||||
version = '1.0'
|
||||
description = '`amixer` command parser'
|
||||
author = 'Eden Refael'
|
||||
author_email = 'edenraf@hotmail.com'
|
||||
compatible = ['linux']
|
||||
magic_commands = ['amixer']
|
||||
tags = ['command']
|
||||
|
||||
|
||||
__version__ = info.version
|
||||
|
||||
|
||||
def _process(proc_data: dict) -> dict:
|
||||
"""
|
||||
Processes raw structured data to match the schema requirements.
|
||||
|
||||
Parameters:
|
||||
proc_data: (dict) raw structured data from the parser
|
||||
|
||||
Returns:
|
||||
(dict) processed structured data adhering to the schema
|
||||
"""
|
||||
# Initialize the processed dictionary
|
||||
processed = {
|
||||
"control_name": proc_data.get("control_name", ""),
|
||||
"capabilities": proc_data.get("capabilities", []),
|
||||
"playback_channels": proc_data.get("playback_channels", []),
|
||||
"limits": {
|
||||
"playback_min": convert_to_int(proc_data.get("limits", {}).get("playback_min", 0)),
|
||||
"playback_max": convert_to_int(proc_data.get("limits", {}).get("playback_max", 0)),
|
||||
},
|
||||
}
|
||||
|
||||
# Process Mono or channel-specific data
|
||||
channels = ["mono", "front_left", "front_right"]
|
||||
for channel in channels:
|
||||
if channel in proc_data:
|
||||
channel_data = proc_data[channel]
|
||||
processed[channel] = {
|
||||
"playback_value": convert_to_int(channel_data.get("playback_value", 0)),
|
||||
"percentage": convert_to_int(channel_data.get("percentage", "0%").strip("%")),
|
||||
"db": float(channel_data.get("db", "0.0db").strip("db")),
|
||||
"status": channel_data.get("status", "off") == "on",
|
||||
}
|
||||
|
||||
return processed
|
||||
|
||||
|
||||
def parse(
|
||||
data: str,
|
||||
raw: bool = False,
|
||||
quiet: bool = False
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
Main text parsing function, The amixer is alsa mixer tool and output, Will work with Linux OS only.
|
||||
|
||||
|
||||
Parameters:
|
||||
data: (string) text data to parse
|
||||
raw: (boolean) unprocessed output if True
|
||||
quiet: (boolean) suppress warning messages if True
|
||||
|
||||
|
||||
Returns:
|
||||
List of Dictionaries. Raw or processed structured data.
|
||||
push test
|
||||
"""
|
||||
"""
|
||||
The Algorithm for parsing the `amixer sget` command, Input Explained/Rules/Pseudo Algorithm:
|
||||
1. There will always be the first line which tells the user about the control name.
|
||||
2. There will always be the Capabilities which include many of capabilities - It will be listed and separated by `" "`.
|
||||
3. After that we'll need to distinct between the Channel - Could be many of channels - It will be listed and separated
|
||||
by `" "`.
|
||||
3a. Capture channels - List of channels
|
||||
3b. Playback channels - List of channels
|
||||
4. Limits - We'll always have the minimum limit and the maximum limit.
|
||||
|
||||
|
||||
Input Example:
|
||||
1."":~$ amixer sget Capture
|
||||
Simple mixer control 'Capture',0
|
||||
Capabilities: cvolume cswitch
|
||||
Capture channels: Front Left - Front Right
|
||||
Limits: Capture 0 - 63
|
||||
Front Left: Capture 63 [100%] [30.00db] [on]
|
||||
Front Right: Capture 63 [100%] [30.00db] [on]
|
||||
|
||||
|
||||
|
||||
|
||||
2."":~$ amixer sget Master
|
||||
Simple mixer control 'Master',0
|
||||
Capabilities: pvolume pvolume-joined pswitch pswitch-joined
|
||||
Playback channels: Mono
|
||||
Limits: Playback 0 - 87
|
||||
Mono: Playback 87 [100%] [0.00db] [on]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
3."":~$ amixer sget Speaker
|
||||
Simple mixer control 'Speaker',0
|
||||
Capabilities: pvolume pswitch
|
||||
Playback channels: Front Left - Front Right
|
||||
Limits: Playback 0 - 87
|
||||
Mono:
|
||||
Front Left: Playback 87 [100%] [0.00db] [on]
|
||||
Front Right: Playback 87 [100%] [0.00db] [on]
|
||||
|
||||
|
||||
|
||||
|
||||
4."":~$ amixer sget Headphone
|
||||
Simple mixer control 'Headphone',0
|
||||
Capabilities: pvolume pswitch
|
||||
Playback channels: Front Left - Front Right
|
||||
Limits: Playback 0 - 87
|
||||
Mono:
|
||||
Front Left: Playback 0 [0%] [-65.25db] [off]
|
||||
Front Right: Playback 0 [0%] [-65.25db] [off]
|
||||
"""
|
||||
# checks os compatibility and print a stderr massage if not compatible. quiet True could remove this check.
|
||||
jc.utils.compatibility(__name__, info.compatible, quiet)
|
||||
|
||||
# check if string
|
||||
jc.utils.input_type_check(data)
|
||||
|
||||
# starts the parsing from here
|
||||
mapping = {}
|
||||
# split lines and than work on each line
|
||||
lines = data.splitlines()
|
||||
first_line = lines[0].strip()
|
||||
|
||||
# Extract the control name from the first line
|
||||
if first_line.startswith("Simple mixer control"):
|
||||
control_name = first_line.split("'")[1]
|
||||
else:
|
||||
raise ValueError("Invalid amixer output format: missing control name.")
|
||||
# map the control name
|
||||
mapping["control_name"] = control_name
|
||||
|
||||
# Process subsequent lines for capabilities, channels, limits, and channel-specific mapping.
|
||||
# gets the lines from the next line - because we already took care the first line.
|
||||
for line in lines[1:]:
|
||||
# strip the line (maybe there are white spaces in the begin&end)
|
||||
line = line.strip()
|
||||
|
||||
if line.startswith("Capabilities:"):
|
||||
mapping["capabilities"] = line.split(":")[1].strip().split()
|
||||
elif line.startswith("Playback channels:"):
|
||||
mapping["playback_channels"] = line.split(":")[1].strip().split(" - ")
|
||||
elif line.startswith("Limits:"):
|
||||
limits = line.split(":")[1].strip().split(" - ")
|
||||
mapping["limits"] = {
|
||||
"playback_min": limits[0].split()[1],
|
||||
"playback_max": limits[1]
|
||||
}
|
||||
elif line.startswith("Mono:") or line.startswith("Front Left:") or line.startswith("Front Right:"):
|
||||
# Identify the channel name and parse its information
|
||||
channel_name = line.split(":")[0].strip().lower().replace(" ", "_")
|
||||
channel_info = line.split(":")[1].strip()
|
||||
# Example: "Playback 255 [100%] [0.00db] [on]"
|
||||
channel_data = channel_info.split(" ")
|
||||
if channel_data[0] == "":
|
||||
continue
|
||||
playback_value = channel_data[1]
|
||||
percentage = channel_data[2].strip("[]") # Extract percentage e.g., "100%"
|
||||
db_value = channel_data[3].strip("[]") # Extract db value e.g., "0.00db"
|
||||
status = channel_data[4].strip("[]") # Extract status e.g., "on" or "off"
|
||||
|
||||
# Store channel mapping in the dictionary
|
||||
mapping[channel_name] = {
|
||||
"playback_value": playback_value,
|
||||
"percentage": percentage,
|
||||
"db": db_value.lower(),
|
||||
"status": status
|
||||
}
|
||||
|
||||
return mapping if raw else _process(mapping)
|
1
tests/fixtures/ubuntu-22.04/amixer-control-capture-processed.json
vendored
Normal file
1
tests/fixtures/ubuntu-22.04/amixer-control-capture-processed.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"control_name":"Capture","capabilities":["cvolume","cswitch"],"playback_channels":[],"limits":{"playback_min":0,"playback_max":63},"front_left":{"playback_value":63,"percentage":100,"db":30.0,"status":true},"front_right":{"playback_value":63,"percentage":100,"db":30.0,"status":true}}
|
1
tests/fixtures/ubuntu-22.04/amixer-control-capture.json
vendored
Normal file
1
tests/fixtures/ubuntu-22.04/amixer-control-capture.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"control_name": "Capture", "capabilities": ["cvolume", "cswitch"], "limits": {"playback_min": "0", "playback_max": "63"}, "front_left": {"playback_value": "63", "percentage": "100%", "db": "30.00db", "status": "on"}, "front_right": {"playback_value": "63", "percentage": "100%", "db": "30.00db", "status": "on"}}
|
6
tests/fixtures/ubuntu-22.04/amixer-control-capture.out
vendored
Normal file
6
tests/fixtures/ubuntu-22.04/amixer-control-capture.out
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
Simple mixer control 'Capture',0
|
||||
Capabilities: cvolume cswitch
|
||||
Capture channels: Front Left - Front Right
|
||||
Limits: Capture 0 - 63
|
||||
Front Left: Capture 63 [100%] [30.00dB] [on]
|
||||
Front Right: Capture 63 [100%] [30.00dB] [on]
|
1
tests/fixtures/ubuntu-22.04/amixer-control-headphone-processed.json
vendored
Normal file
1
tests/fixtures/ubuntu-22.04/amixer-control-headphone-processed.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"control_name":"Headphone","capabilities":["pvolume","pswitch"],"playback_channels":["Front Left","Front Right"],"limits":{"playback_min":0,"playback_max":87},"front_left":{"playback_value":0,"percentage":0,"db":-65.25,"status":false},"front_right":{"playback_value":0,"percentage":0,"db":-65.25,"status":false}}
|
1
tests/fixtures/ubuntu-22.04/amixer-control-headphone.json
vendored
Normal file
1
tests/fixtures/ubuntu-22.04/amixer-control-headphone.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"control_name": "Headphone", "capabilities": ["pvolume", "pswitch"], "playback_channels": ["Front Left", "Front Right"], "limits": {"playback_min": "0", "playback_max": "87"}, "front_left": {"playback_value": "0", "percentage": "0%", "db": "-65.25db", "status": "off"}, "front_right": {"playback_value": "0", "percentage": "0%", "db": "-65.25db", "status": "off"}}
|
7
tests/fixtures/ubuntu-22.04/amixer-control-headphone.out
vendored
Normal file
7
tests/fixtures/ubuntu-22.04/amixer-control-headphone.out
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
Simple mixer control 'Headphone',0
|
||||
Capabilities: pvolume pswitch
|
||||
Playback channels: Front Left - Front Right
|
||||
Limits: Playback 0 - 87
|
||||
Mono:
|
||||
Front Left: Playback 0 [0%] [-65.25dB] [off]
|
||||
Front Right: Playback 0 [0%] [-65.25dB] [off]
|
1
tests/fixtures/ubuntu-22.04/amixer-control-master-processed.json
vendored
Normal file
1
tests/fixtures/ubuntu-22.04/amixer-control-master-processed.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"control_name":"Master","capabilities":["pvolume","pvolume-joined","pswitch","pswitch-joined"],"playback_channels":["Mono"],"limits":{"playback_min":0,"playback_max":87},"mono":{"playback_value":87,"percentage":100,"db":0.0,"status":true}}
|
1
tests/fixtures/ubuntu-22.04/amixer-control-master.json
vendored
Normal file
1
tests/fixtures/ubuntu-22.04/amixer-control-master.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"control_name": "Master", "capabilities": ["pvolume", "pvolume-joined", "pswitch", "pswitch-joined"], "playback_channels": ["Mono"], "limits": {"playback_min": "0", "playback_max": "87"}, "mono": {"playback_value": "87", "percentage": "100%", "db": "0.00db", "status": "on"}}
|
5
tests/fixtures/ubuntu-22.04/amixer-control-master.out
vendored
Normal file
5
tests/fixtures/ubuntu-22.04/amixer-control-master.out
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
Simple mixer control 'Master',0
|
||||
Capabilities: pvolume pvolume-joined pswitch pswitch-joined
|
||||
Playback channels: Mono
|
||||
Limits: Playback 0 - 87
|
||||
Mono: Playback 87 [100%] [0.00dB] [on]
|
1
tests/fixtures/ubuntu-22.04/amixer-control-speakers-processed.json
vendored
Normal file
1
tests/fixtures/ubuntu-22.04/amixer-control-speakers-processed.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"control_name":"Speaker","capabilities":["pvolume","pswitch"],"playback_channels":["Front Left","Front Right"],"limits":{"playback_min":0,"playback_max":87},"front_left":{"playback_value":87,"percentage":100,"db":0.0,"status":true},"front_right":{"playback_value":87,"percentage":100,"db":0.0,"status":true}}
|
1
tests/fixtures/ubuntu-22.04/amixer-control-speakers.json
vendored
Normal file
1
tests/fixtures/ubuntu-22.04/amixer-control-speakers.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"control_name": "Speaker", "capabilities": ["pvolume", "pswitch"], "playback_channels": ["Front Left", "Front Right"], "limits": {"playback_min": "0", "playback_max": "87"}, "front_left": {"playback_value": "87", "percentage": "100%", "db": "0.00db", "status": "on"}, "front_right": {"playback_value": "87", "percentage": "100%", "db": "0.00db", "status": "on"}}
|
7
tests/fixtures/ubuntu-22.04/amixer-control-speakers.out
vendored
Normal file
7
tests/fixtures/ubuntu-22.04/amixer-control-speakers.out
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
Simple mixer control 'Speaker',0
|
||||
Capabilities: pvolume pswitch
|
||||
Playback channels: Front Left - Front Right
|
||||
Limits: Playback 0 - 87
|
||||
Mono:
|
||||
Front Left: Playback 87 [100%] [0.00dB] [on]
|
||||
Front Right: Playback 87 [100%] [0.00dB] [on]
|
48
tests/test_amixer.py
Normal file
48
tests/test_amixer.py
Normal file
@ -0,0 +1,48 @@
|
||||
import unittest
|
||||
import jc.parsers.amixer
|
||||
import os
|
||||
import json
|
||||
|
||||
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
class AmixerTests(unittest.TestCase):
|
||||
AMIXER_CMD = 'amixer'
|
||||
UBUNTU_22_04_TEST_FIXTURES_PATH = f'{THIS_DIR}/fixtures/ubuntu-22.04/'
|
||||
AMIXER_CONTROL_PATH = f'{UBUNTU_22_04_TEST_FIXTURES_PATH}amixer-control-'
|
||||
TEST_FILES_NAME = [
|
||||
f"{AMIXER_CONTROL_PATH}capture",
|
||||
f'{AMIXER_CONTROL_PATH}headphone',
|
||||
f'{AMIXER_CONTROL_PATH}master',
|
||||
f'{AMIXER_CONTROL_PATH}speakers',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
self.test_files_out = [f'{file}.out' for file in self.TEST_FILES_NAME]
|
||||
self.test_files_json = [f'{file}.json' for file in self.TEST_FILES_NAME]
|
||||
self.test_files_processed_json = [f'{file}-processed.json' for file in self.TEST_FILES_NAME]
|
||||
|
||||
def test_amixer_sget(self):
|
||||
for file_out, file_json, file_processed_json in zip(self.test_files_out, self.test_files_json,
|
||||
self.test_files_processed_json):
|
||||
with open(file_out, 'r') as f:
|
||||
amixer_sget_raw_output: str = f.read()
|
||||
with open(file_json, 'r') as f:
|
||||
expected_amixer_sget_json_output: str = f.read()
|
||||
expected_amixer_sget_json_map: dict = json.loads(expected_amixer_sget_json_output)
|
||||
with open(file_processed_json, 'r') as f:
|
||||
expected_amixer_sget_processed_json_output: str = f.read()
|
||||
expected_amixer_sget_processed_json_map: dict = json.loads(expected_amixer_sget_processed_json_output)
|
||||
|
||||
# Tests for raw=True
|
||||
amixer_sget_json_map: dict = jc.parse(self.AMIXER_CMD, amixer_sget_raw_output, raw=True,
|
||||
quiet=True)
|
||||
self.assertEqual(amixer_sget_json_map, expected_amixer_sget_json_map)
|
||||
# Tests for raw=False process
|
||||
amixer_sget_json_processed_map: dict = jc.parse(self.AMIXER_CMD, amixer_sget_raw_output, raw=False,
|
||||
quiet=True)
|
||||
self.assertEqual(amixer_sget_json_processed_map, expected_amixer_sget_processed_json_map)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Reference in New Issue
Block a user