Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

created the amixer sget command parser - READY FOR REVIEW #616

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b3601bb
created the amixer first skeleton
Nov 21, 2024
6a02b3d
push testing and integrate this commit and branch with issue: #591
Nov 21, 2024
27ce3b2
#591 checks the input data with jc utils
Nov 22, 2024
37dc01f
created the data parser of the sget control of the amixer sget <contr…
AvocadoStyle Nov 24, 2024
f3261d3
test commit - just for tests
Nov 25, 2024
2475452
another test commit
Nov 25, 2024
ca62b6a
another test commit
AvocadoStyle Nov 25, 2024
9049805
created a dedicated pseudo algorithm for the amixer sget and tried va…
AvocadoStyle Nov 27, 2024
1002afc
orginized the docstring with general explanation about the tool and t…
AvocadoStyle Nov 27, 2024
d0b8d05
created raw implementation, but it's raw either or either.
AvocadoStyle Nov 27, 2024
2c9d6dc
orginized the content inside the amixer parser
AvocadoStyle Nov 28, 2024
9a4c055
removed endpoint name
AvocadoStyle Nov 28, 2024
b603286
added amixer to the jc parser in lib
AvocadoStyle Nov 28, 2024
f26d286
more explanations
AvocadoStyle Nov 28, 2024
85c9c8e
added tests for the amixer sget
AvocadoStyle Nov 28, 2024
3e39cdc
added tests for the amixer sget
AvocadoStyle Nov 28, 2024
d80d340
Merge branch 'dev' into feature/amixer-new-parser
kellyjonbrazil Nov 29, 2024
8f20ea4
fine versioning fix
AvocadoStyle Nov 29, 2024
8966600
created docstring+another explanations seperated.
AvocadoStyle Nov 29, 2024
1876e2d
created the amixer parser docu
AvocadoStyle Nov 29, 2024
6ae5ae5
Merge branch 'feature/amixer-new-parser' of github.com:AvocadoStyle/j…
AvocadoStyle Nov 29, 2024
58990f3
added the amixer in alphabet order to the json convert lib
AvocadoStyle Dec 3, 2024
241d1a1
Fix PEP 8: E302 violation as part of boy scout principle
AvocadoStyle Dec 3, 2024
99a3757
deleted not necessary file
AvocadoStyle Dec 4, 2024
a2ea7fc
fixed the spaces between sections in the amixer description
AvocadoStyle Dec 4, 2024
d14b273
resolved commits such as amixer module docstring and preperations for…
AvocadoStyle Dec 4, 2024
6741f19
Revert "Fix PEP 8: E302 violation as part of boy scout principle"
AvocadoStyle Dec 4, 2024
b45b1d6
created the dedicated _process for raw=False
AvocadoStyle Dec 4, 2024
ce338f6
created the dedicated _process for raw=False
AvocadoStyle Dec 4, 2024
4b098bc
added tests for the _process raw=False.
AvocadoStyle Dec 4, 2024
f0c710c
changed keys to be lowercase snake-case - Change 'dB' to 'db'
AvocadoStyle Dec 18, 2024
ccb9903
added more dB -> db changes and used int convertor of the jc utils
AvocadoStyle Dec 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jc/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'acpi',
'airport',
'airport-s',
'amixer',
'apt-cache-show',
'apt-get-sqq',
'arp',
Expand Down
277 changes: 277 additions & 0 deletions jc/parsers/amixer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
r"""jc - JSON Convert `amixer sget` command output parser

Usage (cli):
kellyjonbrazil marked this conversation as resolved.
Show resolved Hide resolved

$ 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:
AvocadoStyle marked this conversation as resolved.
Show resolved Hide resolved

{
"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
AvocadoStyle marked this conversation as resolved.
Show resolved Hide resolved
{
"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
AvocadoStyle marked this conversation as resolved.
Show resolved Hide resolved

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 = '[email protected]'
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 = {}
AvocadoStyle marked this conversation as resolved.
Show resolved Hide resolved
# split lines and than work on each line
lines = data.splitlines()
AvocadoStyle marked this conversation as resolved.
Show resolved Hide resolved
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)
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions tests/fixtures/ubuntu-22.04/amixer-control-capture.json
Original file line number Diff line number Diff line change
@@ -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 changes: 6 additions & 0 deletions tests/fixtures/ubuntu-22.04/amixer-control-capture.out
Original file line number Diff line number Diff line change
@@ -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]
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions tests/fixtures/ubuntu-22.04/amixer-control-headphone.json
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions tests/fixtures/ubuntu-22.04/amixer-control-headphone.out
Original file line number Diff line number Diff line change
@@ -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]
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions tests/fixtures/ubuntu-22.04/amixer-control-master.json
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions tests/fixtures/ubuntu-22.04/amixer-control-master.out
Original file line number Diff line number Diff line change
@@ -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]
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions tests/fixtures/ubuntu-22.04/amixer-control-speakers.json
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions tests/fixtures/ubuntu-22.04/amixer-control-speakers.out
Original file line number Diff line number Diff line change
@@ -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 changes: 48 additions & 0 deletions tests/test_amixer.py
Original file line number Diff line number Diff line change
@@ -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()
Loading