Skip to content

utils

Classes:

Name Description
CloneProgress

Handler providing an interface to parse progress information emitted by git.

KeyRequest

Map LCD button or G-Key with an abstract request model.

Functions:

Name Description
check_bios_ver

Check the DSC-BIOS release version.

check_dcs_bios_entry

Check DCS-BIOS entry in Export.lua file.

check_dcs_ver

Check DCS version and release type.

check_github_repo

Update git repository.

check_ver_at_github

Check a version of / at GitHub.

collect_debug_data

Collect and zip all data for troubleshooting.

count_files

Count files with extension in a directory.

detect_system_color_mode

Detect the color mode of the system.

download_file

Download a file from URL and save to save_path.

generate_bios_jsons_with_lupa

Regenerate DCS-BIOS JSON files.

get_all_git_refs

Get a list of branches and tags for repo.

get_config_yaml_location

Get a location of YAML configuration files.

get_default_yaml

Return a full path to the default configuration file.

get_depiction_of_ctrls

Get the depiction of controls.

get_full_bios_for_plane

Collect full BIOS for plane with name.

get_inputs_for_plane

Get dict with all not empty inputs for plane.

get_key_instance

Resolve the provided key string into an instance of a valid key class based on a predefined set of classes and their respective resolution methods.

get_list_of_ctrls

Get a list of all controllers from dict with sections and inputs.

get_plane_aliases

Get a list of all YAML files for plane with name.

get_planes_list

Get a list of all DCS-BIOS supported planes with clickable cockpit.

get_version_string

Generate formatted string with version number.

is_git_exec_present

Check if the git executable is present in a system.

is_git_object

Check if git_obj is a valid Git reference.

is_git_repo

Check if dir_path ios Git repository.

is_git_sha

Check if a ref is a git commit SHA.

load_json

Load JSON from a file into a dictionary.

load_yaml

Load YAML from a file into a dictionary.

replace_symbols

Replace symbols in a string with specified replacements.

rgb

Convert a Color instance to its RGB components as a tuple of integers.

rgba

Convert a color to a single integer or tuple of integers.

run_command

Run command in shell as a subprocess.

save_yaml

Save disc as YAML file.

substitute_symbols

Substitute symbols in a string with specified replacements.

verify_hashes

Check hashes for a file.

CloneProgress

CloneProgress(progress, stage)

Bases: RemoteProgress

Handler providing an interface to parse progress information emitted by git.

Initialize the progress handler.

Parameters:

Name Type Description Default

progress

Progress Qt6 signal

required

stage

Report stage Qt6 signal

required

Methods:

Name Description
get_curr_op

Get a stage name from OP code.

update

Call whenever the progress changes.

Source code in src/dcspy/utils.py
411
412
413
414
415
416
417
418
419
420
def __init__(self, progress, stage) -> None:
    """
    Initialize the progress handler.

    :param progress: Progress Qt6 signal
    :param stage: Report stage Qt6 signal
    """
    super().__init__()
    self.progress_signal = progress
    self.stage_signal = stage

get_curr_op

get_curr_op(op_code: int) -> str

Get a stage name from OP code.

Parameters:

Name Type Description Default

op_code

int

OP code

required

Returns:

Type Description
str

stage name

Source code in src/dcspy/utils.py
422
423
424
425
426
427
428
429
430
def get_curr_op(self, op_code: int) -> str:
    """
    Get a stage name from OP code.

    :param op_code: OP code
    :return: stage name
    """
    op_code_masked = op_code & self.OP_MASK
    return self.OP_CODE_MAP.get(op_code_masked, '?').title()

update

update(
    op_code: int,
    cur_count,
    max_count=None,
    message: str = "",
) -> None

Call whenever the progress changes.

Parameters:

Name Type Description Default

op_code

int

Integer allowing to be compared against Operation IDs and stage IDs.

required

cur_count

A count of current absolute items

required

max_count

The maximum count of items we expect. It may be None in case there is no maximum number of items or if it is (yet) unknown.

None

message

str

It contains the number of bytes transferred. It may be used for other purposes as well.

''
Source code in src/dcspy/utils.py
432
433
434
435
436
437
438
439
440
441
442
443
444
445
def update(self, op_code: int, cur_count, max_count=None, message: str = '') -> None:
    """
    Call whenever the progress changes.

    :param op_code: Integer allowing to be compared against Operation IDs and stage IDs.
    :param cur_count: A count of current absolute items
    :param max_count: The maximum count of items we expect. It may be None in case there is no maximum number of items or if it is (yet) unknown.
    :param message: It contains the number of bytes transferred. It may be used for other purposes as well.
    """
    if op_code & git.RemoteProgress.BEGIN:
        self.stage_signal.emit(f'Git clone: {self.get_curr_op(op_code)}')

    percentage = int(cur_count / max_count * 100) if max_count else 0
    self.progress_signal.emit(percentage)

KeyRequest

KeyRequest(
    yaml_path: Path, get_bios_fn: Callable[[str], BiosValue]
)

Map LCD button or G-Key with an abstract request model.

Load YAML with BIOS request for G-Keys and LCD buttons.

Parameters:

Name Type Description Default

yaml_path

Path

Path to the airplane YAML file.

required

get_bios_fn

Callable[[str], BiosValue]

Function used to get a current BIOS value.

required

Methods:

Name Description
get_request

Get abstract representation for request ti be sent for requested button.

set_request

Update the internal string request for the specified button.

Attributes:

Name Type Description
cycle_button_ctrl_name dict[str, int]

Return a dictionary with BIOS selectors to track changes of values for a cycle button to get current values.

Source code in src/dcspy/utils.py
780
781
782
783
784
785
786
787
788
789
790
791
792
def __init__(self, yaml_path: Path, get_bios_fn: Callable[[str], BiosValue]) -> None:
    """
    Load YAML with BIOS request for G-Keys and LCD buttons.

    :param yaml_path: Path to the airplane YAML file.
    :param get_bios_fn: Function used to get a current BIOS value.
    """
    plane_yaml = load_yaml(full_path=yaml_path)
    self.buttons: dict[AnyButton, RequestModel] = {}
    for key_str, request in plane_yaml.items():
        if request:
            key = get_key_instance(key_str)
            self.buttons[key] = RequestModel.from_request(key=key, request=request, get_bios_fn=get_bios_fn)

cycle_button_ctrl_name property

cycle_button_ctrl_name: dict[str, int]

Return a dictionary with BIOS selectors to track changes of values for a cycle button to get current values.

get_request

get_request(button: AnyButton) -> RequestModel

Get abstract representation for request ti be sent for requested button.

Parameters:

Name Type Description Default

button

AnyButton

LcdButton, Gkey or MouseButton

required

Returns:

Type Description
RequestModel

RequestModel object

Source code in src/dcspy/utils.py
799
800
801
802
803
804
805
806
def get_request(self, button: AnyButton) -> RequestModel:
    """
    Get abstract representation for request ti be sent for requested button.

    :param button: LcdButton, Gkey or MouseButton
    :return: RequestModel object
    """
    return self.buttons.get(button, RequestModel.make_empty(key=button))

set_request

set_request(button: AnyButton, req: str) -> None

Update the internal string request for the specified button.

Parameters:

Name Type Description Default

button

AnyButton

LcdButton, Gkey or MouseButton

required

req

str

The raw request to set.

required
Source code in src/dcspy/utils.py
808
809
810
811
812
813
814
815
def set_request(self, button: AnyButton, req: str) -> None:
    """
    Update the internal string request for the specified button.

    :param button: LcdButton, Gkey or MouseButton
    :param req: The raw request to set.
    """
    self.buttons[button].raw_request = req

check_bios_ver

check_bios_ver(bios_path: Path | str) -> Version

Check the DSC-BIOS release version.

Parameters:

Name Type Description Default

bios_path

Path | str

Path to DCS-BIOS directory in the SavedGames folder

required

Returns:

Type Description
Version

Version object

Source code in src/dcspy/utils.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def check_bios_ver(bios_path: Path | str) -> version.Version:
    """
    Check the DSC-BIOS release version.

    :param bios_path: Path to DCS-BIOS directory in the SavedGames folder
    :return: Version object
    """
    bios_ver = version.parse('0.0.0')
    new_location = Path(bios_path) / 'lib' / 'modules' / 'common_modules' / 'CommonData.lua'
    old_location = Path(bios_path) / 'lib' / 'CommonData.lua'

    if new_location.is_file():
        with open(file=new_location, encoding='utf-8') as cd_lua:
            cd_lua_data = cd_lua.read()
    elif old_location.is_file():
        with open(file=old_location, encoding='utf-8') as cd_lua:
            cd_lua_data = cd_lua.read()
    else:
        cd_lua_data = ''
        LOG.debug(f'No `CommonData.lua` while checking DCS-BIOS version at {new_location.parent} or {old_location.parent}')

    if bios_re := search(r'function getVersion\(\)\s*return\s*\"([\d.]*)\"', cd_lua_data):
        bios_ver = version.parse(bios_re.group(1))
    return bios_ver

check_dcs_bios_entry

check_dcs_bios_entry(
    lua_dst_data: str, lua_dst_path: Path, temp_dir: Path
) -> str

Check DCS-BIOS entry in Export.lua file.

Parameters:

Name Type Description Default

lua_dst_data

str

Content of Export.lua

required

lua_dst_path

Path

Export.lua path

required

temp_dir

Path

Directory with DCS-BIOS archive

required

Returns:

Type Description
str

Result of checks

Source code in src/dcspy/utils.py
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
def check_dcs_bios_entry(lua_dst_data: str, lua_dst_path: Path, temp_dir: Path) -> str:
    """
    Check DCS-BIOS entry in Export.lua file.

    :param lua_dst_data: Content of Export.lua
    :param lua_dst_path: Export.lua path
    :param temp_dir: Directory with DCS-BIOS archive
    :return: Result of checks
    """
    result = '\n\nExport.lua exists.'
    lua = 'Export.lua'
    with open(file=temp_dir / lua, encoding='utf-8') as lua_src:
        lua_src_data = lua_src.read()
    export_re = search(r'dofile\(lfs.writedir\(\)\s*\.\.\s*\[\[Scripts\\DCS-BIOS\\BIOS\.lua]]\)', lua_dst_data)
    if not export_re:
        with open(file=lua_dst_path / lua, mode='a+', encoding='utf-8') as exportlua_dst:
            exportlua_dst.write(f'\n{lua_src_data}')
        LOG.debug(f'Add DCS-BIOS to Export.lua: {lua_src_data}')
        result += '\n\nDCS-BIOS entry added.\n\nYou verify installation at:\ngithub.com/DCS-Skunkworks/DCSFlightpanels/wiki/Installation'
    else:
        result += '\n\nDCS-BIOS entry detected.'
    return result

check_dcs_ver

check_dcs_ver(dcs_path: Path) -> str

Check DCS version and release type.

Parameters:

Name Type Description Default

dcs_path

Path

Path to DCS installation directory

required

Returns:

Type Description
str

DCS version as strings

Source code in src/dcspy/utils.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def check_dcs_ver(dcs_path: Path) -> str:
    """
    Check DCS version and release type.

    :param dcs_path: Path to DCS installation directory
    :return: DCS version as strings
    """
    result_ver = 'Unknown'
    try:
        with open(file=dcs_path / 'autoupdate.cfg', encoding='utf-8') as autoupdate_cfg:
            autoupdate_data = autoupdate_cfg.read()
    except (FileNotFoundError, PermissionError) as err:
        LOG.debug(f'{type(err).__name__}: {err.filename}')
    else:
        if dcs_ver := search(r'"version":\s"([\d.]*)"', autoupdate_data):
            result_ver = str(dcs_ver.group(1))
    return result_ver

check_github_repo

check_github_repo(
    git_ref: str,
    repo_dir: Path,
    repo: str,
    update: bool = True,
    progress: RemoteProgress | None = None,
) -> str

Update git repository.

Return SHA of the latest commit.

Parameters:

Name Type Description Default

git_ref

str

Any Git reference as a string

required

repo_dir

Path

Local directory for a repository

required

repo

str

GitHub repository address

required

update

bool

Perform update process

True

progress

RemoteProgress | None

Progress callback

None
Source code in src/dcspy/utils.py
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def check_github_repo(git_ref: str, repo_dir: Path, repo: str, update: bool = True, progress: git.RemoteProgress | None = None) -> str:
    """
    Update git repository.

    Return SHA of the latest commit.

    :param git_ref: Any Git reference as a string
    :param repo_dir: Local directory for a repository
    :param repo: GitHub repository address
    :param update: Perform update process
    :param progress: Progress callback
    """
    bios_repo = _checkout_repo(git_ref=git_ref, repo_dir=repo_dir, repo=repo, progress=progress)
    is_detached = bios_repo.head.is_detached
    LOG.debug(f'Repo at: {git_ref} with HEAD detached: {is_detached}')
    if update and not is_detached:
        f_info = bios_repo.remotes[0].pull(progress=progress)
        LOG.debug(f'Pulled: {f_info[0].name} as: {f_info[0].commit}')
    git_ref =  git_ref.split('/')[1] if 'origin/' in git_ref else git_ref
    sha = _get_sha_hex_str(bios_repo, git_ref)
    return sha

check_ver_at_github

check_ver_at_github(repo: str) -> Release

Check a version of / at GitHub.

Parameters:

Name Type Description Default

repo

str

Format '/'

required

Returns:

Type Description
Release

Release object with data

Source code in src/dcspy/utils.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def check_ver_at_github(repo: str) -> Release:
    """
    Check a version of <organization>/<package> at GitHub.

    :param repo: Format '<organization or user>/<package>'
    :return: Release object with data
    """
    package = repo.split('/')[1]
    try:
        response = get(url=f'https://api.github.com/repos/{repo}/releases/latest', timeout=5)
        if response.ok:
            rel = Release(**response.json())
            LOG.debug(f'Latest GitHub release: {rel}')
            return rel
        else:
            raise ValueError(f'Try again later. Status={response.status_code}')
    except Exception as exc:
        raise ValueError(f'Unable to check {package} version online: {exc}')

collect_debug_data

collect_debug_data() -> Path

Collect and zip all data for troubleshooting.

Returns:

Type Description
Path

Path object to ZIP file

Source code in src/dcspy/utils.py
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
def collect_debug_data() -> Path:
    """
    Collect and zip all data for troubleshooting.

    :return: Path object to ZIP file
    """
    config_file = Path(get_config_yaml_location() / CONFIG_YAML).resolve()
    conf_dict = load_yaml(config_file)
    sys_data = _get_sys_file(conf_dict)
    dcs_log = _get_dcs_log(conf_dict)

    zip_file = Path(gettempdir()) / f'dcspy_debug_{str(datetime.now()).replace(" ", "_").replace(":", "")}.zip'
    with zipfile.ZipFile(file=zip_file, mode='w', compresslevel=9, compression=zipfile.ZIP_DEFLATED) as archive:
        archive.write(sys_data, arcname=sys_data.name)
        archive.write(dcs_log, arcname=dcs_log.name)
        for log_file in _get_log_files():
            archive.write(log_file, arcname=log_file.name)
        for yaml_file in _get_yaml_files(config_file):
            archive.write(yaml_file, arcname=yaml_file.name)
        for png in _get_png_files():
            archive.write(png, arcname=png.name)

    return zip_file

count_files

count_files(directory: Path, extension: str) -> int

Count files with extension in a directory.

Parameters:

Name Type Description Default

directory

Path

As Path object

required

extension

str

File extension

required

Returns:

Type Description
int

Number of files

Source code in src/dcspy/utils.py
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
def count_files(directory: Path, extension: str) -> int:
    """
    Count files with extension in a directory.

    :param directory: As Path object
    :param extension: File extension
    :return: Number of files
    """
    try:
        json_files = [f.name for f in directory.iterdir() if f.is_file() and f.suffix == f'.{extension}']
        LOG.debug(f'In: {directory} found {json_files} ')
        return len(json_files)
    except FileNotFoundError:
        LOG.debug(f'Wrong directory: {directory}')
        return -1

detect_system_color_mode

detect_system_color_mode() -> str

Detect the color mode of the system.

Registry will return 0 if Windows is in Dark Mode and 1 if Windows is in Light Mode. In case of error, it will return 'light' as default.

Returns:

Type Description
str

Dark or light as string

Source code in src/dcspy/utils.py
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
def detect_system_color_mode() -> str:
    """
    Detect the color mode of the system.

    Registry will return 0 if Windows is in Dark Mode and 1 if Windows is in Light Mode.
    In case of error, it will return 'light' as default.

    :return: Dark or light as string
    """
    from winreg import HKEY_CURRENT_USER, OpenKey, QueryValueEx  # type: ignore[attr-defined]

    try:
        key = OpenKey(HKEY_CURRENT_USER, 'Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize')
        subkey = QueryValueEx(key, 'AppsUseLightTheme')[0]
    except (OSError, IndexError):
        return 'Light'
    return {0: 'Dark', 1: 'Light'}[subkey]

download_file

download_file(
    url: str,
    save_path: Path,
    progress_fn: Callable[[int], None] | None = None,
) -> bool

Download a file from URL and save to save_path.

Parameters:

Name Type Description Default

url

str

URL address

required

save_path

Path

full path to save

required

progress_fn

Callable[[int], None] | None

a callable object to report download progress

None
Source code in src/dcspy/utils.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def download_file(url: str, save_path: Path, progress_fn: Callable[[int], None] | None = None) -> bool:
    """
    Download a file from URL and save to save_path.

    :param url: URL address
    :param save_path: full path to save
    :param progress_fn: a callable object to report download progress
    """
    response = get(url=url, stream=True, timeout=5)
    if response.ok:
        file_size = int(response.headers.get('Content-Length', 0))
        LOG.debug(f'File size: {file_size / (1024 * 1024):.2f} MB' if file_size else 'File size: Unknown')
        LOG.debug(f'Download file from: {url}')
        with open(save_path, 'wb+') as dl_file:
            downloaded = 0
            progress = 0
            for chunk in response.iter_content(chunk_size=1024):
                dl_file.write(chunk)
                downloaded += len(chunk)
                new_progress = int((downloaded / file_size) * 100)
                if progress_fn and new_progress == progress + 1:
                    progress = new_progress
                    progress_fn(progress)
            LOG.debug(f'Saved as: {save_path}')
            return True
    else:
        LOG.warning(f'Can not download from: {url}')
        return False

generate_bios_jsons_with_lupa

generate_bios_jsons_with_lupa(
    dcs_save_games: Path,
    local_compile="./Scripts/DCS-BIOS/test/compile/LocalCompile.lua",
) -> None

Regenerate DCS-BIOS JSON files.

Using the Lupa library, first it will try to use LuaJIT 2.1 if not, it will fall back to Lua 5.1

Parameters:

Name Type Description Default

dcs_save_games

Path

Full path to the Saved Games\DCS directory.

required

local_compile

Relative path to the LocalCompile.lua file.

'./Scripts/DCS-BIOS/test/compile/LocalCompile.lua'
Source code in src/dcspy/utils.py
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
def generate_bios_jsons_with_lupa(dcs_save_games: Path, local_compile='./Scripts/DCS-BIOS/test/compile/LocalCompile.lua') -> None:
    r"""
    Regenerate DCS-BIOS JSON files.

    Using the Lupa library, first it will try to use LuaJIT 2.1 if not, it will fall back to Lua 5.1

    :param dcs_save_games: Full path to the Saved Games\DCS directory.
    :param local_compile: Relative path to the LocalCompile.lua file.
    """
    try:
        import lupa.luajit21 as lupa
    except ImportError:
        try:
            import lupa.lua51 as lupa  # type: ignore[no-redef]
        except ImportError:
            return

    previous_dir = getcwd()
    try:
        chdir(dcs_save_games)
        LOG.debug(f"Changed to: {dcs_save_games}")
        lua = lupa.LuaRuntime()
        LOG.debug(f"Using {lupa.LuaRuntime().lua_implementation} (compiled with {lupa.LUA_VERSION})")
        with open(local_compile) as lua_file:
            lua_script = lua_file.read()
        lua.execute(lua_script)
    finally:
        chdir(previous_dir)
        LOG.debug(f"Change directory back to: {getcwd()}")

get_all_git_refs

get_all_git_refs(repo_dir: Path) -> list[str]

Get a list of branches and tags for repo.

Parameters:

Name Type Description Default

repo_dir

Path

Directory with a repository

required

Returns:

Type Description
list[str]

List of git references as strings

Source code in src/dcspy/utils.py
392
393
394
395
396
397
398
399
400
401
402
403
def get_all_git_refs(repo_dir: Path) -> list[str]:
    """
    Get a list of branches and tags for repo.

    :param repo_dir: Directory with a repository
    :return: List of git references as strings
    """
    refs = []
    if is_git_repo(str(repo_dir)):
        for ref in git.Repo(repo_dir).refs:
            refs.append(str(ref))
    return refs

get_config_yaml_location

get_config_yaml_location() -> Path

Get a location of YAML configuration files.

Source code in src/dcspy/utils.py
583
584
585
586
587
588
589
590
591
def get_config_yaml_location() -> Path:
    """
    Get a location of YAML configuration files.

    :rtype: Path object to directory
    """
    localappdata = environ.get('LOCALAPPDATA', None)
    user_appdata = Path(localappdata) / 'dcspy' if localappdata else DEFAULT_YAML_FILE.parent
    return user_appdata

get_default_yaml

get_default_yaml(local_appdata: bool = False) -> Path

Return a full path to the default configuration file.

Parameters:

Name Type Description Default

local_appdata

bool

If True value C:/Users//AppData/Local is used

False

Returns:

Type Description
Path

Path like an object

Source code in src/dcspy/utils.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def get_default_yaml(local_appdata: bool = False) -> Path:
    """
    Return a full path to the default configuration file.

    :param local_appdata: If True value C:/Users/<user_name>/AppData/Local is used
    :return: Path like an object
    """
    cfg_ful_path = DEFAULT_YAML_FILE
    if local_appdata:
        user_appdata = get_config_yaml_location()
        makedirs(name=user_appdata, exist_ok=True)
        cfg_ful_path = Path(user_appdata / CONFIG_YAML).resolve()
        if not cfg_ful_path.exists():
            save_yaml(data=defaults_cfg, full_path=cfg_ful_path)
    return cfg_ful_path

get_depiction_of_ctrls

get_depiction_of_ctrls(
    inputs: dict[str, dict[str, ControlKeyData]],
) -> dict[str, ControlDepiction]

Get the depiction of controls.

Parameters:

Name Type Description Default

inputs

dict[str, dict[str, ControlKeyData]]

Dictionary with ControlKeyData

required

Returns:

Type Description
dict[str, ControlDepiction]

A dictionary containing the depiction of controls.

Source code in src/dcspy/utils.py
697
698
699
700
701
702
703
704
705
706
707
708
def get_depiction_of_ctrls(inputs: dict[str, dict[str, ControlKeyData]]) -> dict[str, ControlDepiction]:
    """
    Get the depiction of controls.

    :param inputs: Dictionary with ControlKeyData
    :return: A dictionary containing the depiction of controls.
    """
    result = {}
    for section, controllers in inputs.items():
        for ctrl_name, ctrl in controllers.items():
            result[ctrl_name] = ctrl.depiction
    return result

get_full_bios_for_plane cached

get_full_bios_for_plane(
    plane: str, bios_dir: Path
) -> DcsBiosPlaneData

Collect full BIOS for plane with name.

Parameters:

Name Type Description Default

plane

str

BIOS plane name

required

bios_dir

Path

path to DCS-BIOS directory

required

Returns:

Type Description
DcsBiosPlaneData

dict

Source code in src/dcspy/utils.py
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
@lru_cache
def get_full_bios_for_plane(plane: str, bios_dir: Path) -> DcsBiosPlaneData:
    """
    Collect full BIOS for plane with name.

    :param plane: BIOS plane name
    :param bios_dir: path to DCS-BIOS directory
    :return: dict
    """
    alias_path = bios_dir / 'doc' / 'json' / 'AircraftAliases.json'
    local_json: dict[str, Any] = {}
    aircraft_aliases = load_json(full_path=alias_path)
    for json_file in aircraft_aliases[plane]:
        local_json = {**local_json, **load_json(full_path=bios_dir / 'doc' / 'json' / f'{json_file}.json')}

    return DcsBiosPlaneData.model_validate(local_json)

get_inputs_for_plane cached

get_inputs_for_plane(
    plane: str, bios_dir: Path
) -> dict[str, dict[str, ControlKeyData]]

Get dict with all not empty inputs for plane.

Parameters:

Name Type Description Default

plane

str

BIOS plane name

required

bios_dir

Path

path to DCS-BIOS

required

Returns:

Type Description
dict[str, dict[str, ControlKeyData]]

dict.

Source code in src/dcspy/utils.py
640
641
642
643
644
645
646
647
648
649
650
651
@lru_cache
def get_inputs_for_plane(plane: str, bios_dir: Path) -> dict[str, dict[str, ControlKeyData]]:
    """
    Get dict with all not empty inputs for plane.

    :param plane: BIOS plane name
    :param bios_dir: path to DCS-BIOS
    :return: dict.
    """
    plane_bios = get_full_bios_for_plane(plane=plane, bios_dir=bios_dir)
    inputs = plane_bios.get_inputs()
    return inputs

get_key_instance

get_key_instance(key_str: str) -> AnyButton

Resolve the provided key string into an instance of a valid key class based on a predefined set of classes and their respective resolution methods.

If the key string matches a class method's criteria, it returns the resolved key instance. If no match is found, an exception is raised.

Parameters:

Name Type Description Default

key_str

str

A string representing the name or identifier of the key to be resolved into a key instance (e.g., Gkey, LcdButton, or MouseButton).

required

Returns:

Type Description
AnyButton

An instance of a class (AnyButton) that corresponds to the provided key string, if successfully resolved.

Raises:

Type Description
AttributeError

If the provided key string cannot be resolved into a valid key instance using the predefined classes and methods.

Source code in src/dcspy/utils.py
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
def get_key_instance(key_str: str) -> AnyButton:
    """
    Resolve the provided key string into an instance of a valid key class based on a predefined set of classes and their respective resolution methods.

    If the key string matches a class method's criteria, it returns the resolved key instance.
    If no match is found, an exception is raised.

    :param key_str: A string representing the name or identifier of the key to be resolved into a key instance (e.g., Gkey, LcdButton, or MouseButton).
    :return: An instance of a class (AnyButton) that corresponds to the provided key string, if successfully resolved.
    :raises AttributeError: If the provided key string cannot be resolved into a valid key instance using the predefined classes and methods.
    """
    for klass, method in [(Gkey, 'from_yaml'), (MouseButton, 'from_yaml'), (LcdButton, key_str)]:
        key_instance = _try_key_instance(klass=klass, method=method, key_str=key_str)
        if key_instance:
            return key_instance
    raise AttributeError(f'Could not resolve "{key_str}" to a Gkey/LcdButton/MouseButton instance')

get_list_of_ctrls

get_list_of_ctrls(
    inputs: dict[str, dict[str, ControlKeyData]],
) -> list[str]

Get a list of all controllers from dict with sections and inputs.

Parameters:

Name Type Description Default

inputs

dict[str, dict[str, ControlKeyData]]

Dictionary with ControlKeyData

required

Returns:

Type Description
list[str]

List of string

Source code in src/dcspy/utils.py
654
655
656
657
658
659
660
661
662
663
664
665
666
def get_list_of_ctrls(inputs: dict[str, dict[str, ControlKeyData]]) -> list[str]:
    """
    Get a list of all controllers from dict with sections and inputs.

    :param inputs: Dictionary with ControlKeyData
    :return: List of string
    """
    result_list = []
    for section, controllers in inputs.items():
        result_list.append(f'{CTRL_LIST_SEPARATOR} {section} {CTRL_LIST_SEPARATOR}')
        for ctrl_name in controllers:
            result_list.append(ctrl_name)
    return result_list

get_plane_aliases cached

get_plane_aliases(
    bios_dir: Path, plane: str | None = None
) -> dict[str, list[str]]

Get a list of all YAML files for plane with name.

Parameters:

Name Type Description Default

plane

str | None

BIOS plane name

None

bios_dir

Path

path to DCS-BIOS

required

Returns:

Type Description
dict[str, list[str]]

list of all YAML files for plane definition

Source code in src/dcspy/utils.py
681
682
683
684
685
686
687
688
689
690
691
692
693
694
@lru_cache
def get_plane_aliases(bios_dir: Path, plane: str | None = None) -> dict[str, list[str]]:
    """
    Get a list of all YAML files for plane with name.

    :param plane: BIOS plane name
    :param bios_dir: path to DCS-BIOS
    :return: list of all YAML files for plane definition
    """
    alias_path = bios_dir / 'doc' / 'json' / 'AircraftAliases.json'
    aircraft_aliases = load_json(full_path=alias_path)
    if plane:
        aircraft_aliases = {plane: aircraft_aliases[plane]}
    return aircraft_aliases

get_planes_list cached

get_planes_list(bios_dir: Path) -> list[str]

Get a list of all DCS-BIOS supported planes with clickable cockpit.

Parameters:

Name Type Description Default

bios_dir

Path

Path to DCS-BIOS

required

Returns:

Type Description
list[str]

List of all supported planes

Source code in src/dcspy/utils.py
669
670
671
672
673
674
675
676
677
678
@lru_cache
def get_planes_list(bios_dir: Path) -> list[str]:
    """
    Get a list of all DCS-BIOS supported planes with clickable cockpit.

    :param bios_dir: Path to DCS-BIOS
    :return: List of all supported planes
    """
    aircraft_aliases = get_plane_aliases(bios_dir=bios_dir, plane=None)
    return [name for name, yaml_data in aircraft_aliases.items() if yaml_data not in (['CommonData', 'FC3'], ['CommonData'])]

get_version_string

get_version_string(
    repo: str,
    current_ver: str | Version,
    check: bool = True,
) -> str

Generate formatted string with version number.

Parameters:

Name Type Description Default

repo

str

Format '/'.

required

current_ver

str | Version

String or Version object.

required

check

bool

Version online.

True

Returns:

Type Description
str

Formatted version as a string.

Source code in src/dcspy/utils.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
def get_version_string(repo: str, current_ver: str | version.Version, check: bool = True) -> str:
    """
    Generate formatted string with version number.

    :param repo: Format '<organization or user>/<package>'.
    :param current_ver: String or Version object.
    :param check: Version online.
    :return: Formatted version as a string.
    """
    ver_string = f'v{current_ver}'
    if check:
        try:
            details = ''
            result = check_ver_at_github(repo=repo)
        except ValueError:
            return f'v{current_ver} (failed)'

        if result.is_latest(current_ver=current_ver):
            details = ' (latest)'
        elif result.version != version.parse('0.0.0'):
            details = ' (update!)'
            current_ver = result.version
        ver_string = f'v{current_ver}{details}'
    return ver_string

is_git_exec_present

is_git_exec_present() -> bool

Check if the git executable is present in a system.

Returns:

Type Description
bool

True if git.exe is available

Source code in src/dcspy/utils.py
358
359
360
361
362
363
364
365
366
367
368
369
def is_git_exec_present() -> bool:
    """
    Check if the git executable is present in a system.

    :return: True if git.exe is available
    """
    try:
        import git
        return bool(git.GIT_OK)
    except ImportError as err:
        LOG.debug(type(err).__name__, exc_info=True)
        return False

is_git_object

is_git_object(repo_dir: Path, git_obj: str) -> bool

Check if git_obj is a valid Git reference.

Parameters:

Name Type Description Default

repo_dir

Path

Directory with repository

required

git_obj

str

Git reference to check

required

Returns:

Type Description
bool

True if git_obj is git reference, False otherwise

Source code in src/dcspy/utils.py
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
def is_git_object(repo_dir: Path, git_obj: str) -> bool:
    """
    Check if git_obj is a valid Git reference.

    :param repo_dir: Directory with repository
    :param git_obj: Git reference to check
    :return: True if git_obj is git reference, False otherwise
    """
    import gitdb  # type: ignore[import-untyped]
    result = False
    if is_git_repo(str(repo_dir)):
        all_refs = get_all_git_refs(repo_dir=repo_dir)
        if git_obj in all_refs:
            result = True
        with suppress(gitdb.exc.BadName, TypeError, ValueError):
            git.Repo(repo_dir).commit(git_obj)
            result = True
    return result

is_git_repo

is_git_repo(dir_path: str) -> bool

Check if dir_path ios Git repository.

Parameters:

Name Type Description Default

dir_path

str

Path as string

required

Returns:

Type Description
bool

True if dir is a git repo

Source code in src/dcspy/utils.py
210
211
212
213
214
215
216
217
218
219
220
221
222
def is_git_repo(dir_path: str) -> bool:
    """
    Check if dir_path ios Git repository.

    :param dir_path: Path as string
    :return: True if dir is a git repo
    """
    import git
    try:
        _ = git.Repo(dir_path).git_dir
        return True
    except (git.InvalidGitRepositoryError, git.exc.NoSuchPathError):
        return False

is_git_sha

is_git_sha(repo: Repo, ref: str) -> bool

Check if a ref is a git commit SHA.

Parameters:

Name Type Description Default

repo

Repo

Git Repository object

required

ref

str

Git commit SHA as string

required

Returns:

Type Description
bool

True if ref is SHA of git commit, False otherwise

Source code in src/dcspy/utils.py
301
302
303
304
305
306
307
308
309
310
311
312
313
314
def is_git_sha(repo: git.Repo, ref: str) -> bool:
    """
    Check if a ref is a git commit SHA.

    :param repo: Git Repository object
    :param ref: Git commit SHA as string
    :return: True if ref is SHA of git commit, False otherwise
    """
    import gitdb  # type: ignore[import-untyped]

    with suppress(gitdb.exc.BadName):
        _  = repo.commit(ref).hexsha
        return True
    return False

load_json

load_json(full_path: Path) -> dict[Any, Any]

Load JSON from a file into a dictionary.

Parameters:

Name Type Description Default

full_path

Path

Full path

required

Returns:

Type Description
dict[Any, Any]

Python representation of JSON

Source code in src/dcspy/utils.py
610
611
612
613
614
615
616
617
618
619
def load_json(full_path: Path) -> dict[Any, Any]:
    """
    Load JSON from a file into a dictionary.

    :param full_path: Full path
    :return: Python representation of JSON
    """
    with open(full_path, encoding='utf-8') as json_file:
        data = json_file.read()
    return json.loads(data)

load_yaml

load_yaml(full_path: Path) -> DcspyConfigYaml

Load YAML from a file into a dictionary.

Parameters:

Name Type Description Default

full_path

Path

Full path to YAML file

required

Returns:

Type Description
DcspyConfigYaml

Dictionary with data

Source code in src/dcspy/utils.py
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def load_yaml(full_path: Path) -> DcspyConfigYaml:
    """
    Load YAML from a file into a dictionary.

    :param full_path: Full path to YAML file
    :return: Dictionary with data
    """
    try:
        with open(file=full_path, encoding='utf-8') as yaml_file:
            data = yaml.load(yaml_file, Loader=yaml.SafeLoader)
            if not isinstance(data, dict):
                data = {}
    except (FileNotFoundError, yaml.parser.ParserError) as err:
        makedirs(name=full_path.parent, exist_ok=True)
        LOG.warning(f'{type(err).__name__}: {full_path}.', exc_info=True)
        LOG.debug(f'{err}')
        data = {}
    return data

replace_symbols

replace_symbols(
    value: str, symbol_replacement: Sequence[Sequence[str]]
) -> str

Replace symbols in a string with specified replacements.

Parameters:

Name Type Description Default

value

str

The string in which symbols will be replaced.

required

symbol_replacement

Sequence[Sequence[str]]

A sequence of sequences containing the original symbols and their replacement strings.

required

Returns:

Type Description
str

The string with symbols to replace.

Source code in src/dcspy/utils.py
724
725
726
727
728
729
730
731
732
733
734
def replace_symbols(value: str, symbol_replacement: Sequence[Sequence[str]]) -> str:
    """
    Replace symbols in a string with specified replacements.

    :param value: The string in which symbols will be replaced.
    :param symbol_replacement: A sequence of sequences containing the original symbols and their replacement strings.
    :return: The string with symbols to replace.
    """
    for original, replacement in symbol_replacement:
        value = value.replace(original, replacement)
    return value

rgb

rgb(c: Color) -> tuple[int, int, int]

Convert a Color instance to its RGB components as a tuple of integers.

The function extracts the red, green, and blue components from the color's value, which is expected to be a single integer representing a 24-bit RGB color.

Parameters:

Name Type Description Default

c

Color

An instance of Color, whose value is a 24-bit RGB integer.

required

Returns:

Type Description
tuple[int, int, int]

A tuple containing the red, green, and blue components.

Source code in src/dcspy/utils.py
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
def rgb(c: Color, /) -> tuple[int, int, int]:
    """
    Convert a Color instance to its RGB components as a tuple of integers.

    The function extracts the red, green, and blue components from the
    color's value, which is expected to be a single integer representing
    a 24-bit RGB color.

    :param c: An instance of Color, whose value is a 24-bit RGB integer.
    :return: A tuple containing the red, green, and blue components.
    """
    red = (c.value >> 16) & 0xff
    green = (c.value >> 8) & 0xff
    blue = c.value & 0xff
    return red, green, blue

rgba

rgba(
    c: Color, /, mode: LcdMode | int = TRUE_COLOR
) -> tuple[int, ...] | int

Convert a color to a single integer or tuple of integers.

This depends on a given mode/alpha channel: * If a mode is an integer, then return a tuple of RGBA channels. * If a mode is a LcdMode.TRUE_COLOR, then return a tuple of RGBA channels. * If a mode is a LcdMode.BLACK_WHITE, then return a single integer.

Parameters:

Name Type Description Default

c

Color

Color name to convert

required

mode

LcdMode | int

Mode of the LCD or alpha channel as an integer

TRUE_COLOR

Returns:

Type Description
tuple[int, ...] | int

tuple with RGBA channels or single integer

Source code in src/dcspy/utils.py
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
def rgba(c: Color, /, mode: LcdMode | int = LcdMode.TRUE_COLOR) -> tuple[int, ...] | int:
    """
    Convert a color to a single integer or tuple of integers.

    This depends on a given mode/alpha channel:
    * If a mode is an integer, then return a tuple of RGBA channels.
    * If a mode is a LcdMode.TRUE_COLOR, then return a tuple of RGBA channels.
    * If a mode is a LcdMode.BLACK_WHITE, then return a single integer.

    :param c: Color name to convert
    :param mode: Mode of the LCD or alpha channel as an integer
    :return: tuple with RGBA channels or single integer
    """
    if isinstance(mode, int):
        return *rgb(c), mode
    else:
        return ImageColor.getcolor(color=c.name, mode=mode.value)

run_command

run_command(
    cmd: Sequence[str], cwd: Path | None = None
) -> int

Run command in shell as a subprocess.

Parameters:

Name Type Description Default

cmd

Sequence[str]

The command to be executed as a sequence of strings

required

cwd

Path | None

current working directory

None

Returns:

Type Description
int

The return code of command

Source code in src/dcspy/utils.py
594
595
596
597
598
599
600
601
602
603
604
605
606
607
def run_command(cmd: Sequence[str], cwd: Path | None = None) -> int:
    """
    Run command in shell as a subprocess.

    :param cmd: The command to be executed as a sequence of strings
    :param cwd: current working directory
    :return: The return code of command
    """
    try:
        proc = run(cmd, check=True, shell=False, cwd=cwd)
        return proc.returncode
    except CalledProcessError as e:
        LOG.debug(f'Result: {e}')
        return -1

save_yaml

save_yaml(data: DcspyConfigYaml, full_path: Path) -> None

Save disc as YAML file.

Parameters:

Name Type Description Default

data

DcspyConfigYaml

Dictionary with data

required

full_path

Path

Full a path to YAML file

required
Source code in src/dcspy/utils.py
78
79
80
81
82
83
84
85
86
def save_yaml(data: DcspyConfigYaml, full_path: Path) -> None:
    """
    Save disc as YAML file.

    :param data: Dictionary with data
    :param full_path: Full a path to YAML file
    """
    with open(file=full_path, mode='w', encoding='utf-8') as yaml_file:
        yaml.dump(data, yaml_file, Dumper=yaml.SafeDumper)

substitute_symbols

substitute_symbols(
    value: str, symbol_replacement: Sequence[Sequence[str]]
) -> str

Substitute symbols in a string with specified replacements.

Parameters:

Name Type Description Default

value

str

The input string to be processed

required

symbol_replacement

Sequence[Sequence[str]]

A list of symbol patterns and their corresponding replacements.

required

Returns:

Type Description
str

The processed string with symbols is replaced according to the provided symbol_replacement list.

Source code in src/dcspy/utils.py
711
712
713
714
715
716
717
718
719
720
721
def substitute_symbols(value: str, symbol_replacement: Sequence[Sequence[str]]) -> str:
    """
    Substitute symbols in a string with specified replacements.

    :param value: The input string to be processed
    :param symbol_replacement: A list of symbol patterns and their corresponding replacements.
    :return: The processed string with symbols is replaced according to the provided symbol_replacement list.
    """
    for pattern, replacement in symbol_replacement:
        value = sub(pattern, replacement, value)
    return value

verify_hashes

verify_hashes(
    file_path: Path, digest_file: Path
) -> tuple[bool, dict[str, bool]]

Check hashes for a file.

Parameters:

Name Type Description Default

file_path

Path

Path to the file

required

digest_file

Path

Path to the digests file

required

Returns:

Type Description
tuple[bool, dict[str, bool]]

Overall verdict and detailed results

Source code in src/dcspy/utils.py
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
def verify_hashes(file_path: Path, digest_file: Path) -> tuple[bool, dict[str, bool]]:
    """
    Check hashes for a file.

    :param file_path: Path to the file
    :param digest_file: Path to the digests file
    :return: Overall verdict and detailed results
    """
    if not file_path.is_file() or not digest_file.is_file():
        return False, {}

    with open(digest_file) as f_digests:
        all_digests = f_digests.readlines()

    hashes: dict[str, dict[str, str]] = {}
    for line in all_digests:
        if line.startswith('#HASH'):
            hash_type = line.split()[1]
        elif line.strip():
            hash_and_file = line.split()
            filename = hash_and_file[1] if len(hash_and_file) > 1 else ''
            hashes.setdefault(filename, {})[hash_type] = hash_and_file[0]
    LOG.debug(f'Supported algorithms are: {hashlib.algorithms_guaranteed}')
    results = _compute_hash_and_check_file(file_path=file_path, hashes=hashes)

    return all(results.values()), results