diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..5ca7137 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: ampledata +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: ampledata +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: https://www.buymeacoffee.com/ampledata diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index aa72265..035a5ba 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -30,9 +30,10 @@ jobs: - name: Build Debian/Apt bdist_deb run: | + export REPO_NAME=$(echo ${{ github.repository }} | awk -F"/" '{print $2}') python3 setup.py --command-packages=stdeb.command bdist_deb ls -al deb_dist/ - cp deb_dist/python3-inrcot_*_all.deb deb_dist/python3-inrcot_latest_all.deb + cp deb_dist/python3-${REPO_NAME}_*_all.deb deb_dist/python3-${REPO_NAME}_latest_all.deb - uses: actions/upload-artifact@master with: diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish_to_pypi.yml similarity index 56% rename from .github/workflows/python-publish.yml rename to .github/workflows/python-publish_to_pypi.yml index f6c0270..2c719cf 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish_to_pypi.yml @@ -1,9 +1,10 @@ # This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries +name: Publish package to PyPI -name: Publish package to PYPI - -on: push +on: + push: + tags: + - '*' jobs: deploy: @@ -18,13 +19,12 @@ jobs: python-version: '3.x' - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine + python3 -m pip install --upgrade pip + python3 -m pip install setuptools wheel twine - name: Build run: | - python setup.py sdist bdist_wheel + python3 setup.py sdist bdist_wheel - name: Publish package - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml deleted file mode 100644 index 8b4c0ab..0000000 --- a/.github/workflows/python-test.yml +++ /dev/null @@ -1,51 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a -# single version of Python -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: Lint, Style & Test - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - build: - - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: [3.6, 3.7, 3.8, 3.9] - - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Upgrade PIP - run: | - python -m pip install --upgrade pip - - - name: Install Python testing dependencies - run: | - make install_requirements_test - - - name: Install Python program in 'develop' mode - run: | - make install_requirements develop - - - name: Lint with pylint - run: | - make lint - - - name: Style-check with flake8 - run: | - make flake8 - - - name: Test with pytest - run: | - make pytest diff --git a/.github/workflows/python-test_and_lint.yml b/.github/workflows/python-test_and_lint.yml new file mode 100644 index 0000000..455c927 --- /dev/null +++ b/.github/workflows/python-test_and_lint.yml @@ -0,0 +1,38 @@ +name: Lint & Test Code + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install test requirements + run: | + make install_test_requirements + - name: Install package itself (editable) + run: | + make editable + - name: Lint with pylint + run: | + make pylint + - name: Lint with flake8 + run: | + make flake8 + - name: Test with pytest-cov + run: | + make test_cov diff --git a/.gitignore b/.gitignore index 70b6ce1..5d31ffd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.vscode/ *.deb *.egg *.egg-info/ @@ -29,8 +30,5 @@ output.xml pylint.log redis-server.log redis-server/ -.python-version -venv/ -config.ini - -*/__pycache__/ +__pycache__ +.ipynb_checkpoints/ diff --git a/LICENSE b/LICENSE index ad1faa2..24b9c09 100644 --- a/LICENSE +++ b/LICENSE @@ -1,10 +1,8 @@ -Copyright 2021 Greg Albrecht +Copyright 2022 Greg Albrecht Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 +You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/Makefile b/Makefile index c5556b6..8803c02 100644 --- a/Makefile +++ b/Makefile @@ -1,75 +1,78 @@ -# Makefile for Python Programs - +# +# Copyright 2022 Greg Albrecht +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Author:: Greg Albrecht W2GMD +# Copyright:: Copyright 2022 Greg Albrecht +# License:: Apache License, Version 2.0 +# + +this_app = pytak .DEFAULT_GOAL := all +all: editable -all: develop - -install_requirements: - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi +develop: + python3 setup.py develop -install_requirements_test: - pip install -r requirements_test.txt +editable: + python3 -m pip install -e . -develop: remember - pip install -e . +install_test_requirements: + python3 -m pip install -r requirements_test.txt -install: remember - python setup.py install +install: + python3 setup.py install uninstall: - pip uninstall -y inrcot + python3 -m pip uninstall -y $(this_app) reinstall: uninstall install -remember: - @echo - @echo "Hello from the Makefile..." - @echo "Don't forget to run: 'make install_requirements'" - @echo - -remember_test: - @echo - @echo "Hello from the Makefile..." - @echo "Don't forget to run: 'make install_requirements_test'" - @echo +publish: + python3 setup.py publish clean: @rm -rf *.egg* build dist *.py[oc] */*.py[co] cover doctest_pypi.cfg \ nosetests.xml pylint.log output.xml flake8.log tests.log \ - test-result.xml htmlcov fab.log .coverage */__pycache__/ - -# Publishing: - -build: remember_test - python3 -m build --sdist --wheel - -twine_check: remember_test build - twine check dist/* - -upload: remember_test build - twine upload dist/* + test-result.xml htmlcov fab.log .coverage __pycache__ \ + */__pycache__ -publish: build twine_check upload - -# Tests: - -pep8: remember_test - # flake8 --max-complexity 12 --exit-zero *.py */*.py - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics +pep8: + flake8 --max-line-length=88 --extend-ignore=E203,E231 --exit-zero $(this_app)/*.py flake8: pep8 -lint: remember_test +lint: pylint --msg-template="{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" \ - -r n *.py */*.py || exit 0 + --max-line-length=88 -r n $(this_app)/*.py || exit 0 pylint: lint -pytest: remember_test +checkmetadata: + python3 setup.py check -s --restructuredtext + +mypy: + mypy --strict . + +pytest: pytest -test: lint pep8 pytest +test: editable install_test_requirements pytest + +test_cov: + pytest --cov=$(this_app) + +black: + black . diff --git a/README.rst b/README.rst index c0a5d56..5a442a9 100644 --- a/README.rst +++ b/README.rst @@ -34,16 +34,15 @@ Use Cases ========= There are numerous applications for satellite based position location information, -including: +including:: 1. Blue Force Tracking 2. Search & Rescue (SAR) 3. Partner Forces PLI 4. Asset Tracking 5. Data diode, CDS & cybersecurity considerations -6. Wildland Firefighting / Dingell Act -``inrcot`` may also be of use in wildland firefighting, see Section 1114.d of +INRCOT may also be of use in wildland firefighting, see Section 1114.d of the `Dingell Act `_:: Location Systems for Wildland Firefighters.-- diff --git a/inrcot/__init__.py b/inrcot/__init__.py index a77fb21..b913c1f 100644 --- a/inrcot/__init__.py +++ b/inrcot/__init__.py @@ -21,17 +21,16 @@ ~~~~ :author: Greg Albrecht W2GMD -:copyright: Copyright 2021 Greg Albrecht +:copyright: Copyright 2022 Greg Albrecht :license: Apache License, Version 2.0 :source: """ -from .constants import (LOG_FORMAT, LOG_LEVEL, DEFAULT_POLL_INTERVAL, # NOQA - DEFAULT_COT_STALE, DEFAULT_COT_TYPE) +from .constants import DEFAULT_POLL_INTERVAL, DEFAULT_COT_STALE, DEFAULT_COT_TYPE -from .functions import inreach_to_cot, split_feed # NOQA +from .functions import create_tasks, inreach_to_cot, split_feed -from .classes import InrWorker # NOQA +from .classes import Worker __author__ = "Greg Albrecht W2GMD " __copyright__ = "Copyright 2022 Greg Albrecht" diff --git a/inrcot/classes.py b/inrcot/classes.py index 77d2146..b14fde3 100644 --- a/inrcot/classes.py +++ b/inrcot/classes.py @@ -16,7 +16,7 @@ # Author:: Greg Albrecht W2GMD # -"""inReach Cursor-on-Target Class Definitions.""" +"""INRCOT Class Definitions.""" import asyncio @@ -25,19 +25,20 @@ import pytak import inrcot + __author__ = "Greg Albrecht W2GMD " -__copyright__ = "Copyright 202s Greg Albrecht" +__copyright__ = "Copyright 2022 Greg Albrecht" __license__ = "Apache License, Version 2.0" -class InrWorker(pytak.QueueWorker): +class Worker(pytak.QueueWorker): """Reads inReach Feed, renders to CoT, and puts on a TX queue.""" - def __init__(self, queue: asyncio.Queue, config) -> None: + def __init__(self, queue: asyncio.Queue, config, original_config) -> None: super().__init__(queue, config) self.inreach_feeds: list = [] - self._create_feeds(config) + self._create_feeds(original_config) def _create_feeds(self, config: dict = None) -> None: """Creates a list of feed configurations.""" @@ -90,18 +91,18 @@ async def get_inreach_feeds(self): return if response.status == 200: - await self.handle_response(await response.content.read(), + await self.handle_data(await response.content.read(), feed_conf) else: self._logger.error("No valid response from inReach API.") async def run(self) -> None: """Runs this Worker, Reads from Pollers.""" - self._logger.info("Running InrWorker") + self._logger.info("Run: %s", self.__class__) - poll_interval: int = int(self.config.get( - "POLL_INTERVAL", inrcot.DEFAULT_POLL_INTERVAL)) + poll_interval: str = self.config.get( + "POLL_INTERVAL", inrcot.DEFAULT_POLL_INTERVAL) while 1: await self.get_inreach_feeds() - await asyncio.sleep(poll_interval) + await asyncio.sleep(int(poll_interval)) diff --git a/inrcot/commands.py b/inrcot/commands.py index 192ca9e..4e53152 100644 --- a/inrcot/commands.py +++ b/inrcot/commands.py @@ -15,83 +15,106 @@ # # Author:: Greg Albrecht W2GMD # -"""inReach Cursor-on-Target Gateway Commands.""" -import asyncio +"""PyTAK Command Line.""" + import argparse -import configparser +import asyncio +import importlib import logging +import os +import platform import sys -import urllib + +from configparser import ConfigParser, SectionProxy import pytak -import inrcot +__author__ = "Greg Albrecht W2GMD " +__copyright__ = "Copyright 2022 Greg Albrecht" +__license__ = "Apache License, Version 2.0" + # Python 3.6 support: if sys.version_info[:2] >= (3, 7): from asyncio import get_running_loop else: - from asyncio import _get_running_loop as get_running_loop # NOQA pylint: disable=no-name-in-module - - -__author__ = "Greg Albrecht W2GMD " -__copyright__ = "Copyright 2021 Greg Albrecht." -__license__ = "Apache License, Version 2.0" - - -async def main(config): - """Main program function, executes workers, et al.""" - tx_queue: asyncio.Queue = asyncio.Queue() - rx_queue: asyncio.Queue = asyncio.Queue() - - cot_url: urllib.parse.ParseResult = urllib.parse.urlparse( - config["inrcot"].get("COT_URL")) + from asyncio import get_event_loop as get_running_loop + + +async def main(app_name: str, config: SectionProxy, original_config: ConfigParser) -> None: + """ + Abstract implementation of an async main function. + + Parameters + ---------- + app_name : `str` + Name of the app calling this function. + config : `SectionProxy` + A dict of configuration parameters & values. + """ + app = importlib.__import__(app_name) + clitool: pytak.CLITool = pytak.CLITool(config) + create_tasks = getattr(app, "create_tasks") + await clitool.setup() + clitool.add_tasks(create_tasks(config, clitool, original_config)) + await clitool.run() + + +def cli(app_name: str = "inrcot") -> None: + """ + Abstract implementation of a Command Line Interface (CLI). + + Parameters + ---------- + app_name : `str` + Name of the app calling this function. + """ + app = importlib.__import__(app_name) - # Create our CoT Event Queue Worker - reader, writer = await pytak.protocol_factory(cot_url) - write_worker = pytak.EventTransmitter(tx_queue, writer) - read_worker = pytak.EventReceiver(rx_queue, reader) - - message_worker = inrcot.InrWorker(tx_queue, config) - - await tx_queue.put(pytak.hello_event("inrcot")) - - done, _ = await asyncio.wait( - {message_worker.run(), read_worker.run(), write_worker.run()}, - return_when=asyncio.FIRST_COMPLETED) - - for task in done: - print(f"Task completed: {task}") - - -def cli(): - """Command Line interface for inReach Cursor-on-Target Gateway.""" - - # Get cli arguments: parser = argparse.ArgumentParser() - - parser.add_argument("-c", "--CONFIG_FILE", dest="CONFIG_FILE", - default="config.ini", type=str) + parser.add_argument( + "-c", + "--CONFIG_FILE", + dest="CONFIG_FILE", + default="config.ini", + type=str, + help="Optional configuration file. Default: config.ini", + ) namespace = parser.parse_args() cli_args = {k: v for k, v in vars(namespace).items() if v is not None} - # Read config file: - config = configparser.ConfigParser() + # Read config: + env_vars = os.environ + env_vars["COT_URL"] = env_vars.get("COT_URL", pytak.DEFAULT_COT_URL) + env_vars["COT_HOST_ID"] = f"{app_name}@{platform.node()}" + env_vars["COT_STALE"] = getattr(app, "DEFAULT_COT_STALE", pytak.DEFAULT_COT_STALE) + config: ConfigParser = ConfigParser(env_vars) config_file = cli_args.get("CONFIG_FILE") - logging.info("Reading configuration from %s", config_file) - config.read(config_file) + if os.path.exists(config_file): + logging.info("Reading configuration from %s", config_file) + config.read(config_file) + else: + config.add_section(app_name) + + original_config: ConfigParser = config + config: SectionProxy = config[app_name] + + debug = config.getboolean("DEBUG") + if debug: + import pprint # pylint: disable=import-outside-toplevel + # FIXME: This doesn't work with weird bash escape stuff. + print("Showing Config: %s", config_file) + print("=" * 10) + pprint.pprint(config) + print("=" * 10) if sys.version_info[:2] >= (3, 7): - asyncio.run(main(config), debug=config["inrcot"].getboolean("DEBUG")) + asyncio.run(main(app_name, config, original_config), debug=debug) else: loop = get_running_loop() try: - loop.run_until_complete(main(config)) + loop.run_until_complete(main(app_name, config, original_config)) finally: loop.close() - - -if __name__ == '__main__': - cli() diff --git a/inrcot/constants.py b/inrcot/constants.py index 1814622..e95f60b 100644 --- a/inrcot/constants.py +++ b/inrcot/constants.py @@ -16,31 +16,18 @@ # Author:: Greg Albrecht W2GMD # -"""inReach to Cursor-on-Target Constants.""" - -import logging -import os +"""INRCOT Constants.""" __author__ = "Greg Albrecht W2GMD " __copyright__ = "Copyright 2021 Greg Albrecht" __license__ = "Apache License, Version 2.0" -if bool(os.environ.get('DEBUG')): - LOG_LEVEL = logging.DEBUG - LOG_FORMAT = logging.Formatter( - ('%(asctime)s inrcot %(levelname)s %(name)s.%(funcName)s:%(lineno)d ' - ' - %(message)s')) - logging.debug('inrcot Debugging Enabled via DEBUG Environment Variable.') -else: - LOG_LEVEL = logging.INFO - LOG_FORMAT = logging.Formatter('%(asctime)s inrcot - %(message)s') - # How long between checking for new messages at the Spot API? DEFAULT_POLL_INTERVAL: int = 120 # How longer after producting the CoT Event is the Event 'stale' (seconds) -DEFAULT_COT_STALE: int = 600 +DEFAULT_COT_STALE: str = "600" # CoT Event Type / 2525 type / SIDC-like -DEFAULT_COT_TYPE: str = "a-.-G-E-V-C" +DEFAULT_COT_TYPE: str = "a-n-G-E-V-C" diff --git a/inrcot/functions.py b/inrcot/functions.py index a6fcb1d..5803cdf 100644 --- a/inrcot/functions.py +++ b/inrcot/functions.py @@ -22,7 +22,8 @@ import io import xml.etree.ElementTree as ET -from typing import Union +from configparser import ConfigParser +from typing import Union, Set import pytak import inrcot @@ -33,6 +34,27 @@ __license__ = "Apache License, Version 2.0" +def create_tasks( + config: ConfigParser, clitool: pytak.CLITool, original_config: ConfigParser +) -> Set[pytak.Worker,]: + """ + Creates specific coroutine task set for this application. + + Parameters + ---------- + config : `ConfigParser` + Configuration options & values. + clitool : `pytak.CLITool` + A PyTAK Worker class instance. + + Returns + ------- + `set` + Set of PyTAK Worker classes for this application. + """ + return set([inrcot.Worker(clitool.tx_queue, config, original_config)]) + + def split_feed(content: str) -> list: """Splits an inReach MapShare KML feed by 'Folder'""" tree = ET.parse(io.BytesIO(content)) diff --git a/requirements_test.txt b/requirements_test.txt index b1daabc..9120a8a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,9 +1,5 @@ -# Python Distribution Package Requirements for Tests - -build -flake8 +pytest-asyncio +pytest-cov pylint -twine -setuptools -wheel -pytest \ No newline at end of file +flake8 +black diff --git a/setup.py b/setup.py index 875a6d0..42b4897 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ def publish(): packages=[__title__], package_dir={__title__: __title__}, url=f"https://github.com/ampledata/{__title__}", - entry_points={"console_scripts": [f"{__title__} = {__title__}.commands:main"]}, + entry_points={"console_scripts": [f"{__title__} = {__title__}.commands:cli"]}, description="inReach Cursor-on-Target Gateway.", author="Greg Albrecht", author_email="oss@undef.net",