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

Add cli #119

Merged
merged 15 commits into from
Jul 13, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Added timeout parameter in pygmc.connect & devices.
- Previously hardcoded to 5 seconds - now a kwarg in all devices and pygmc.connect.
- Added more unittests - to guarantee kwarg consistency.
- Added command-line-interface (CLI)

## 0.13.0 (2024-05-11)
- Added WiFi commands
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ df = pd.DataFrame(history[1:], columns=history[0])
- [GeigerLog](https://sourceforge.net/projects/geigerlog/)
- gq-gmc-control
- gmc
- Device website [GQ Electronics](https://gqelectronicsllc.com/) Seattle, WA
- Device website [GQ Electronics](https://gqelectronicsllc.com/?referer=pygmc) Seattle, WA
- Not affiliated in any way.


Expand Down
32 changes: 32 additions & 0 deletions docs/cli.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
PyGMC CLI
=========


**The preferred method.**

PyGMC has a command-line-interface CLI that becomes available after installed.
Installing pygmc adds an `entry point <https://setuptools.pypa.io/en/latest/userguide/entry_point.html>`_
console script that becomes available as `pygmc` when the python environment is activated.
You can find the script in `<your_venv_path>/bin/pygmc`.



.. code-block:: bash

pygmc --help

----

**The discouraged method**

If you simply copied the code or git clone (instead of installing pygmc) you should
consider installing it. However, if you absolutely need an alternative method...
you can go in the directory where `pygmc` lives and use the cli with the code below.

.. code-block:: bash

python -m pygmc.cli --help


.. autoprogram:: pygmc.cli:parser
:prog: pygmc
7 changes: 6 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

extensions = ["sphinx.ext.viewcode", "sphinx.ext.autodoc", "sphinx.ext.napoleon"]
extensions = [
"sphinx.ext.viewcode",
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
"sphinxcontrib.autoprogram",
]

templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "tests", "examples.py", "tasks"]
Expand Down
File renamed without changes.
File renamed without changes
Binary file added docs/image/pygmc_usage_example_0.9.1.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,22 @@ Install PyGMC from PYPI: https://pypi.org/project/pygmc/

pip install pygmc

Install PyGMC from Conda-Forge: https://anaconda.org/conda-forge/pygmc

.. code-block:: bash

conda install conda-forge::pygmc

Conda version may lag latest pypi version.


Example Usage
-------------

.. image:: image/pygmc_usage_example_0.9.1.gif
:width: 800


Auto discover connected GMC, auto identify baudrate, and auto select correct device.

.. code-block:: python
Expand Down Expand Up @@ -112,6 +124,7 @@ Supported Devices
devices
connection
history
cli
examples
knownissues

Expand Down
11 changes: 10 additions & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
pyserial>=3.4
sphinx-rtd-theme
sphinxcontrib-autoprogram==0.1.9
Sphinx==7.2.6
sphinx-rtd-theme==1.3.0
sphinxcontrib-applehelp==1.0.7
sphinxcontrib-devhelp==1.0.5
sphinxcontrib-htmlhelp==2.0.4
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.6
sphinxcontrib-serializinghtml==1.1.9
166 changes: 165 additions & 1 deletion pygmc/cli.py
Original file line number Diff line number Diff line change
@@ -1 +1,165 @@
# TODO: Add something like >pygmc --live
"""PyGMC CLI - Command-Line-Interface"""

import argparse
import datetime
from pathlib import Path

try:
from .connection import (
Discovery,
get_all_usb_devices,
get_gmc_usb_devices,
)
from .connection.udev_rule_check import UDevRuleCheck
from .devices import (
auto_get_device_from_discovery_details as _auto_get_device_class,
)
except ImportError:

Check warning on line 17 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L17

Added line #L17 was not covered by tests
# Most likely error while trying 'python ./some_path/pygmc/cli.py --help'
# ImportError: attempted relative import beyond top-level package
# The issue is pygmc/history.py is at same level as pygmc/cli.py
print("See documentation for correct usage.")
raise

Check warning on line 22 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L21-L22

Added lines #L21 - L22 were not covered by tests


# Would've liked this in a function... but...
# don't know how to auto-document cli w/o parser out in the open like this... welp...
parser = argparse.ArgumentParser()

parser.add_argument(
"-p",
"--port",
type=str,
help="The USB port/com/dev e.g. /dev/ttyUSB0 Default=None will auto detect a port.",
metavar=None,
)
parser.add_argument(
"-b",
"--baudrate",
type=int,
help="USB communication baudrate Default=None will auto detect the baudrate.",
metavar=None,
)

subparsers = parser.add_subparsers(
title="Actions",
help="PyGMC CLI Actions:" "Common actions for PyGMC",
dest="actions",
)

# ACTION - list usb's
parser_usb = subparsers.add_parser(
"usb", help="List USB devices found - filtered for GMC devices"
)
parser_usb.add_argument(
"--all", action="store_true", help="List all USBs i.e. unfiltered"
)

# ACTION - live cps
parser_live = subparsers.add_parser("live", help="a help")
parser_live.add_argument(
"-t", "--time", type=int, default=10, help="Time is seconds to live print"
)

# ACTION - save history
parser_history = subparsers.add_parser(
"save", help="Save device history. Default is a tidy csv file."
)
parser_history.add_argument(
"-f",
"--file-name",
type=Path,
required=True,
help="File path. e.g. ~/hist.csv (save history as csv in linux home dir)",
)
parser_history.add_argument(
"--raw", action="store_true", help="Save raw as-is/unmodified device history."
)


def _list_usb_flow(show_all=False):
"""Print simple information on USB devices."""
if show_all:
usbs = get_all_usb_devices()

Check warning on line 83 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L82-L83

Added lines #L82 - L83 were not covered by tests
else:
usbs = get_gmc_usb_devices()
for usb in usbs:

Check warning on line 86 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L85-L86

Added lines #L85 - L86 were not covered by tests
# would be nice to drop python3.7 support and use f"{value=}" formatting
print(f"device={usb.device} description={usb.description} hwid={usb.hwid}")

Check warning on line 88 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L88

Added line #L88 was not covered by tests


def _get_gc(args):
"""Get geiger-counter-class with provided info from pygmc Discovery."""
# Ugh... violating D.R.Y.
discover = Discovery(port=args.port, baudrate=args.baudrate, timeout=5)
discovered_devices = discover.get_all_devices()
if len(discovered_devices) == 0:

Check warning on line 96 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L94-L96

Added lines #L94 - L96 were not covered by tests
# Give user direction in case of brltty udev rule blocking GMC USB device
brltty_udev_rule_check = UDevRuleCheck()

Check warning on line 98 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L98

Added line #L98 was not covered by tests
# line below logs & prints info for user to resolve USB connection issue
brltty_udev_rule_check.get_offending_brltty_rules()
raise ConnectionError("No GMC devices found.")

Check warning on line 101 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L100-L101

Added lines #L100 - L101 were not covered by tests

device_details = discovered_devices[0]
device_class = _auto_get_device_class(device_details)
gc = device_class(

Check warning on line 105 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L103-L105

Added lines #L103 - L105 were not covered by tests
port=device_details.port, baudrate=device_details.baudrate, timeout=5
)
return gc

Check warning on line 108 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L108

Added line #L108 was not covered by tests


def _live_flow(args):
"""Print live data (heartbeat)."""
gc = _get_gc(args)
print("PyGMC - Live GMC Heartbeat")
ver = gc.get_version()
print(f"{ver}")
datetime_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"Start={datetime_str} counts={args.time:,}")
gc.heartbeat_live_print(count=args.time)

Check warning on line 119 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L113-L119

Added lines #L113 - L119 were not covered by tests

datetime_finish_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"Finish={datetime_finish_str}")

Check warning on line 122 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L121-L122

Added lines #L121 - L122 were not covered by tests


def _cli_flow_save(args):
"""Save device history as tidy CSV or raw data."""
if args.file_name.exists():

Check warning on line 127 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L127

Added line #L127 was not covered by tests
# Rather give user an error than the alternative... (why you edit my file)
raise FileExistsError(f"File exists: {args.file_name}")
gc = _get_gc(args)
if not args.raw:

Check warning on line 131 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L129-L131

Added lines #L129 - L131 were not covered by tests
# user want's pygmc tidy data... good Lad or Lass or
# Shklee or Shklim or Shkler (Futurama)
gc.save_history_csv(args.file_name)

Check warning on line 134 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L134

Added line #L134 was not covered by tests
else:
# user is hotshot and want's it raw...
gc.save_history_raw(args.file_name)

Check warning on line 137 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L137

Added line #L137 was not covered by tests


def main(argv=None) -> None:
"""
CLI entry point.

Parameters
----------
argv: None | list
Default=None for correct behavior. Main purpose is for unit-tests.

Returns
-------
None

"""
args = parser.parse_args(argv)

if args.actions == "usb":
_list_usb_flow(show_all=args.all)
elif args.actions == "live":
_live_flow(args)
elif args.actions == "save":
_cli_flow_save(args)

Check warning on line 161 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L156-L161

Added lines #L156 - L161 were not covered by tests


if __name__ == "__main__":
main()

Check warning on line 165 in pygmc/cli.py

View check run for this annotation

Codecov / codecov/patch

pygmc/cli.py#L165

Added line #L165 was not covered by tests
24 changes: 15 additions & 9 deletions pygmc/devices/device_rfc1201.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,18 +286,24 @@ def heartbeat_live_print(self, count=60) -> None:

"""
max_ = 0
i = 0
for cps in self.heartbeat_live(count=count):
i += 1
total = 0
for i, cps in enumerate(self.heartbeat_live(count=count)):
if cps > max_:
max_ = cps
# Yea, I'd rather use f-string... just trying to make it compatible with
# older Python versions
# empty leading space for terminal cursor
msg = " cps={cps:<2} | max={max_:<2} | loop={i:<10,}".format(
cps=cps, max_=max_, i=i
)
total += cps
# more than 4 digits for cps is scary... but let's stick to specs
# From https://www.gqelectronicsllc.com/support/GMC_Selection_Guide.htm
# highest cpm value is "999,999" (a strange number, unlike 65,535)
# perhaps due to screen width limitations. let's use same for cps
# 10 spaces (with commas) per sec... like 3 years
# total uses 11 spaces... if you're doing more than 10 cps for three years
# ...

# Why the leading empty space?
# Because the cursor blinker takes up one space and blocks view.
msg = f" cps={cps:<7,} | max={max_:<7,} | total={total:<11,} | loop={i:<10,}"
print(msg, end="\r") # Carriage return - update line we just printed
# Why extra print \n? Because \r at end causes prompt to end-up shifted at end
print("", end="\n") # empty print to move carriage return to next line

def power_off(self) -> None:
Expand Down
24 changes: 15 additions & 9 deletions pygmc/devices/device_rfc1801.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,18 +348,24 @@ def heartbeat_live_print(self, count=60) -> None:

"""
max_ = 0
i = 0
for cps in self.heartbeat_live(count=count):
i += 1
total = 0
for i, cps in enumerate(self.heartbeat_live(count=count)):
if cps > max_:
max_ = cps
# Yea, I'd rather use f-string... just trying to make it compatible with
# older Python versions
# empty leading space for terminal cursor
msg = " cps={cps:<2} | max={max_:<2} | loop={i:<10,}".format(
cps=cps, max_=max_, i=i
)
total += cps
# more than 4 digits for cps is scary... but let's stick to specs
# From https://www.gqelectronicsllc.com/support/GMC_Selection_Guide.htm
# highest cpm value is "999,999" (a strange number, unlike 65,535)
# perhaps due to screen width limitations. let's use same for cps
# 10 spaces (with commas) per sec... like 3 years
# total uses 11 spaces... if you're doing more than 10 cps for three years
# ...

# Why the leading empty space?
# Because the cursor blinker takes up one space and blocks view.
msg = f" cps={cps:<7,} | max={max_:<7,} | total={total:<11,} | loop={i:<10,}"
print(msg, end="\r") # Carriage return - update line we just printed
# Why extra print \n? Because \r at end causes prompt to end-up shifted at end
print("", end="\n") # empty print to move carriage return to next line

def send_key(self, key_number) -> None:
Expand Down
24 changes: 15 additions & 9 deletions pygmc/devices/device_spec404.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,18 +355,24 @@ def heartbeat_live_print(self, count=60) -> None:

"""
max_ = 0
i = 0
for cps in self.heartbeat_live(count=count):
i += 1
total = 0
for i, cps in enumerate(self.heartbeat_live(count=count)):
if cps > max_:
max_ = cps
# Yea, I'd rather use f-string... just trying to make it compatible with
# older Python versions
# empty leading space for terminal cursor
msg = " cps={cps:<2} | max={max_:<2} | loop={i:<10,}".format(
cps=cps, max_=max_, i=i
)
total += cps
# more than 4 digits for cps is scary... but let's stick to specs
# From https://www.gqelectronicsllc.com/support/GMC_Selection_Guide.htm
# highest cpm value is "999,999" (a strange number, unlike 65,535)
# perhaps due to screen width limitations. let's use same for cps
# 10 spaces (with commas) per sec... like 3 years
# total uses 11 spaces... if you're doing more than 10 cps for three years
# ...

# Why the leading empty space?
# Because the cursor blinker takes up one space and blocks view.
msg = f" cps={cps:<7,} | max={max_:<7,} | total={total:<11,} | loop={i:<10,}"
print(msg, end="\r") # Carriage return - update line we just printed
# Why extra print \n? Because \r at end causes prompt to end-up shifted at end
print("", end="\n") # empty print to move carriage return to next line

def send_key(self, key_number) -> None:
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ classifiers = [
]


[project.scripts]
pygmc = "pygmc.cli:main"


[project.urls]
Homepage = "https://github.com/Wikilicious/pygmc"
Documentation = "https://pygmc.readthedocs.io/"
Expand Down
Loading
Loading