diff --git a/.github/workflows/build_tests.yml b/.github/workflows/build_tests.yml index 4433f2a5..f940ea3d 100644 --- a/.github/workflows/build_tests.yml +++ b/.github/workflows/build_tests.yml @@ -48,8 +48,12 @@ jobs: - name: Install package run: | pip install .[all] - - uses: pypa/gh-action-pip-audit@v1.0.0 + - uses: pypa/gh-action-pip-audit@v1.0.7 with: # Ignore setuptools vulnerability we can't do much about + # Ignore requests vulnerability + # Ignore Setuptools vulnerability ignore-vulns: | - GHSA-r9hx-vwmv-q579 \ No newline at end of file + GHSA-r9hx-vwmv-q579 + GHSA-j8r2-6x86-q33q + PYSEC-2022-43012 \ No newline at end of file diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..51c5e776 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,41 @@ +name: Run CodeCov +on: + push: + branches: + - dev + workflow_dispatch: + +jobs: + run: + runs-on: ubuntu-latest + env: + PYTHON: '3.9' + steps: + - uses: actions/checkout@master + - name: Setup Python + uses: actions/setup-python@master + with: + python-version: 3.9 + - name: Install System Dependencies + run: | + sudo apt-get update + sudo apt install python3-dev + python -m pip install build wheel + - name: Install repo + run: | + pip install .[extras] + - name: Generate coverage report + run: | + pip install pytest + pip install pytest-cov + pytest --cov=./test/unittests --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + directory: ./coverage/reports/ + fail_ci_if_error: true + files: ./coverage.xml,!./cache + flags: unittests + name: codecov-umbrella + verbose: true \ No newline at end of file diff --git a/.github/workflows/publish_alpha.yml b/.github/workflows/publish_alpha.yml index 26060b40..3c0ef9d1 100644 --- a/.github/workflows/publish_alpha.yml +++ b/.github/workflows/publish_alpha.yml @@ -57,8 +57,6 @@ jobs: run: | python setup.py sdist bdist_wheel - name: Publish to PyPI - if: False - # TODO: Remove test patch above uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{secrets.PYPI_TOKEN}} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ff7b7de..167cb86e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,131 @@ # Changelog -## [0.0.32](https://github.com/OpenVoiceOS/ovos-utils/tree/0.0.32) (2023-04-18) +## [0.0.33](https://github.com/OpenVoiceOS/ovos-utils/tree/0.0.33) (2023-06-01) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.31a18...0.0.32) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.33a12...0.0.33) + +**Merged pull requests:** + +- Add unit test coverage for SkillApi [\#145](https://github.com/OpenVoiceOS/ovos-utils/pull/145) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.33a12](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.33a12) (2023-05-31) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.33a11...V0.0.33a12) + +**Merged pull requests:** + +- Update docstrings for FileWatcher class and add unit test coverage [\#152](https://github.com/OpenVoiceOS/ovos-utils/pull/152) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.33a11](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.33a11) (2023-05-30) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.33a10...V0.0.33a11) + +**Merged pull requests:** + +- loosen pexpect requirement [\#153](https://github.com/OpenVoiceOS/ovos-utils/pull/153) ([mikejgray](https://github.com/mikejgray)) + +## [V0.0.33a10](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.33a10) (2023-05-29) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.33a9...V0.0.33a10) + +**Fixed bugs:** + +- fix/filewatcher [\#148](https://github.com/OpenVoiceOS/ovos-utils/pull/148) ([JarbasAl](https://github.com/JarbasAl)) + +**Closed issues:** + +- OVOS-Cookbook link goes nowhere useful [\#150](https://github.com/OpenVoiceOS/ovos-utils/issues/150) + +## [V0.0.33a9](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.33a9) (2023-05-24) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.33a8...V0.0.33a9) + +**Implemented enhancements:** + +- Enable Log name and level overrides from envvars [\#147](https://github.com/OpenVoiceOS/ovos-utils/pull/147) ([NeonDaniel](https://github.com/NeonDaniel)) + +**Closed issues:** + +- os.env [\#146](https://github.com/OpenVoiceOS/ovos-utils/issues/146) +- These lines are always showing even if log\_level is set to INFO [\#142](https://github.com/OpenVoiceOS/ovos-utils/issues/142) + +## [V0.0.33a8](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.33a8) (2023-05-17) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.33a7...V0.0.33a8) + +**Merged pull requests:** + +- Add logging to diagnose process lifecycle [\#144](https://github.com/OpenVoiceOS/ovos-utils/pull/144) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.33a7](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.33a7) (2023-05-17) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.33a6...V0.0.33a7) + +**Fixed bugs:** + +- Configurable skills base directory [\#143](https://github.com/OpenVoiceOS/ovos-utils/pull/143) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.33a6](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.33a6) (2023-05-04) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.33a5...V0.0.33a6) + +**Implemented enhancements:** + +- port LF time utils [\#141](https://github.com/OpenVoiceOS/ovos-utils/pull/141) ([emphasize](https://github.com/emphasize)) + +## [V0.0.33a5](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.33a5) (2023-05-01) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.33a4...V0.0.33a5) + +**Implemented enhancements:** + +- feat/skill\_api [\#139](https://github.com/OpenVoiceOS/ovos-utils/pull/139) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.33a4](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.33a4) (2023-05-01) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.33a3...V0.0.33a4) + +**Fixed bugs:** + +- fix/new\_bug [\#138](https://github.com/OpenVoiceOS/ovos-utils/pull/138) ([JarbasAl](https://github.com/JarbasAl)) + +**Closed issues:** + +- Moving ovos-bus-client to requirements.txt? [\#137](https://github.com/OpenVoiceOS/ovos-utils/issues/137) + +**Merged pull requests:** + +- codecov [\#136](https://github.com/OpenVoiceOS/ovos-utils/pull/136) ([JarbasAl](https://github.com/JarbasAl)) +- codecov automation [\#135](https://github.com/OpenVoiceOS/ovos-utils/pull/135) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.33a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.33a3) (2023-04-29) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.33a2...V0.0.33a3) + +**Fixed bugs:** + +- fix/core\_root\_location [\#134](https://github.com/OpenVoiceOS/ovos-utils/pull/134) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.33a2](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.33a2) (2023-04-24) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.33a1...V0.0.33a2) + +## [V0.0.33a1](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.33a1) (2023-04-24) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.32...V0.0.33a1) + +**Implemented enhancements:** + +- feat/padatious\_samples\_in\_bus [\#133](https://github.com/OpenVoiceOS/ovos-utils/pull/133) ([JarbasAl](https://github.com/JarbasAl)) **Merged pull requests:** - Enable release action [\#131](https://github.com/OpenVoiceOS/ovos-utils/pull/131) ([NeonDaniel](https://github.com/NeonDaniel)) +## [V0.0.32](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.32) (2023-04-18) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.31a18...V0.0.32) + ## [V0.0.31a18](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.31a18) (2023-04-18) [Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.31a17...V0.0.31a18) @@ -153,6 +271,547 @@ - Add show input box method for skills [\#109](https://github.com/OpenVoiceOS/ovos-utils/pull/109) ([AIIX](https://github.com/AIIX)) +## [V0.0.30](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.30) (2023-03-09) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.30a4...V0.0.30) + +## [V0.0.30a4](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.30a4) (2023-03-09) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.30a3...V0.0.30a4) + +**Merged pull requests:** + +- Update dependencies to stable versions [\#107](https://github.com/OpenVoiceOS/ovos-utils/pull/107) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.30a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.30a3) (2023-03-08) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.30a2...V0.0.30a3) + +**Merged pull requests:** + +- Bump ovos-config dependency cleanup module init [\#104](https://github.com/OpenVoiceOS/ovos-utils/pull/104) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.30a2](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.30a2) (2023-03-08) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.30a1...V0.0.30a2) + +**Implemented enhancements:** + +- feat/console\_scripts [\#105](https://github.com/OpenVoiceOS/ovos-utils/pull/105) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.30a1](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.30a1) (2023-03-08) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.29...V0.0.30a1) + +**Merged pull requests:** + +- Implement module\_property decorator with unit test [\#103](https://github.com/OpenVoiceOS/ovos-utils/pull/103) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.29](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.29) (2023-03-03) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.29a2...V0.0.29) + +## [V0.0.29a2](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.29a2) (2023-03-03) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.29a1...V0.0.29a2) + +**Fixed bugs:** + +- fix/circular\_import [\#101](https://github.com/OpenVoiceOS/ovos-utils/pull/101) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.29a1](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.29a1) (2023-03-03) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.28...V0.0.29a1) + +**Implemented enhancements:** + +- Migrate/lock monotonic event [\#100](https://github.com/OpenVoiceOS/ovos-utils/pull/100) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.28](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.28) (2023-02-24) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.28a7...V0.0.28) + +## [V0.0.28a7](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.28a7) (2023-02-16) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.28a6...V0.0.28a7) + +**Merged pull requests:** + +- Refactor SSH helpers and add generic systemd helpers [\#95](https://github.com/OpenVoiceOS/ovos-utils/pull/95) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.28a6](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.28a6) (2023-02-15) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.28a5...V0.0.28a6) + +**Merged pull requests:** + +- Handle default network config values if core configuration is incomplete [\#99](https://github.com/OpenVoiceOS/ovos-utils/pull/99) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.28a5](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.28a5) (2023-02-15) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.28a4...V0.0.28a5) + +**Implemented enhancements:** + +- port network utils from mk2 [\#85](https://github.com/OpenVoiceOS/ovos-utils/issues/85) +- improve network checks [\#88](https://github.com/OpenVoiceOS/ovos-utils/pull/88) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.28a4](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.28a4) (2023-02-08) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.28a3...V0.0.28a4) + +**Implemented enhancements:** + +- minor utils fix [\#98](https://github.com/OpenVoiceOS/ovos-utils/pull/98) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.28a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.28a3) (2023-02-07) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.28a2...V0.0.28a3) + +**Implemented enhancements:** + +- feat/runtime\_requirements gui [\#97](https://github.com/OpenVoiceOS/ovos-utils/pull/97) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.28a2](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.28a2) (2023-02-04) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.28a1...V0.0.28a2) + +**Implemented enhancements:** + +- feat/network\_reqs\_from\_workshop [\#96](https://github.com/OpenVoiceOS/ovos-utils/pull/96) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.28a1](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.28a1) (2023-01-25) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.27...V0.0.28a1) + +**Fixed bugs:** + +- According to the usage, you should be able to pass the name to LOG\(\). [\#94](https://github.com/OpenVoiceOS/ovos-utils/pull/94) ([gmsoft-tuxicoman](https://github.com/gmsoft-tuxicoman)) + +## [V0.0.27](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.27) (2023-01-20) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.27a8...V0.0.27) + +## [V0.0.27a8](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.27a8) (2023-01-20) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.27a7...V0.0.27a8) + +**Merged pull requests:** + +- Log deprecation warning in `layers` module [\#93](https://github.com/OpenVoiceOS/ovos-utils/pull/93) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.27a7](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.27a7) (2023-01-12) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.27a6...V0.0.27a7) + +**Merged pull requests:** + +- add transient duration config [\#92](https://github.com/OpenVoiceOS/ovos-utils/pull/92) ([emphasize](https://github.com/emphasize)) + +## [V0.0.27a6](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.27a6) (2023-01-05) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.27a5...V0.0.27a6) + +**Fixed bugs:** + +- fix/mouse\_detect\_again [\#90](https://github.com/OpenVoiceOS/ovos-utils/pull/90) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.27a5](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.27a5) (2022-12-16) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.27a4...V0.0.27a5) + +**Implemented enhancements:** + +- sync utils with core [\#89](https://github.com/OpenVoiceOS/ovos-utils/pull/89) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.27a4](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.27a4) (2022-11-30) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.27a3...V0.0.27a4) + +**Merged pull requests:** + +- Add background\_color to show image and show animated image [\#86](https://github.com/OpenVoiceOS/ovos-utils/pull/86) ([AIIX](https://github.com/AIIX)) + +## [V0.0.27a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.27a3) (2022-11-15) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.27a2...V0.0.27a3) + +**Merged pull requests:** + +- gui notification callback data [\#84](https://github.com/OpenVoiceOS/ovos-utils/pull/84) ([AIIX](https://github.com/AIIX)) + +## [V0.0.27a2](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.27a2) (2022-11-11) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.27a1...V0.0.27a2) + +**Fixed bugs:** + +- fix sudo flag again [\#83](https://github.com/OpenVoiceOS/ovos-utils/pull/83) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.27a1](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.27a1) (2022-11-11) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.26...V0.0.27a1) + +**Fixed bugs:** + +- fix sudo flag [\#82](https://github.com/OpenVoiceOS/ovos-utils/pull/82) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.26](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.26) (2022-10-29) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.26a2...V0.0.26) + +## [V0.0.26a2](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.26a2) (2022-10-22) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.26a1...V0.0.26a2) + +**Implemented enhancements:** + +- refactor some stuff to properties for better compatibility with ovos-… [\#80](https://github.com/OpenVoiceOS/ovos-utils/pull/80) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.26a1](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.26a1) (2022-10-19) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.25...V0.0.26a1) + +**Implemented enhancements:** + +- feat/event\_wrappers\_in\_outils [\#79](https://github.com/OpenVoiceOS/ovos-utils/pull/79) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.25](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.25) (2022-10-18) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.25a15...V0.0.25) + +**Merged pull requests:** + +- license + vulnerability tests [\#78](https://github.com/OpenVoiceOS/ovos-utils/pull/78) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.25a15](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.25a15) (2022-10-18) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.25a14...V0.0.25a15) + +**Fixed bugs:** + +- fix input detect again [\#77](https://github.com/OpenVoiceOS/ovos-utils/pull/77) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.25a14](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.25a14) (2022-10-17) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.25a13...V0.0.25a14) + +**Fixed bugs:** + +- fallback to True for mouse detection if libinput is missing [\#76](https://github.com/OpenVoiceOS/ovos-utils/pull/76) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.25a13](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.25a13) (2022-10-17) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.25a12...V0.0.25a13) + +**Implemented enhancements:** + +- scan /dev/input for device detection [\#75](https://github.com/OpenVoiceOS/ovos-utils/pull/75) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.25a12](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.25a12) (2022-10-17) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.25a11...V0.0.25a12) + +**Fixed bugs:** + +- feat/xinput support [\#74](https://github.com/OpenVoiceOS/ovos-utils/pull/74) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.25a11](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.25a11) (2022-10-11) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.25a10...V0.0.25a11) + +**Merged pull requests:** + +- add mail api point to ovos api service [\#73](https://github.com/OpenVoiceOS/ovos-utils/pull/73) ([AIIX](https://github.com/AIIX)) + +## [V0.0.25a10](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.25a10) (2022-10-10) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.25a9...V0.0.25a10) + +**Closed issues:** + +- `get_mycroft_bus` ignores Configuration [\#71](https://github.com/OpenVoiceOS/ovos-utils/issues/71) + +**Merged pull requests:** + +- Update `ovos_config` references, Read config in `get_mycroft_bus` [\#72](https://github.com/OpenVoiceOS/ovos-utils/pull/72) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.25a9](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.25a9) (2022-10-10) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.25a8...V0.0.25a9) + +**Fixed bugs:** + +- remove "logs" subfolder [\#70](https://github.com/OpenVoiceOS/ovos-utils/pull/70) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.25a8](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.25a8) (2022-10-07) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.25a7...V0.0.25a8) + +**Implemented enhancements:** + +- Update log.py [\#69](https://github.com/OpenVoiceOS/ovos-utils/pull/69) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.25a7](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.25a7) (2022-10-03) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.25a6...V0.0.25a7) + +**Implemented enhancements:** + +- feat/email\_utils [\#68](https://github.com/OpenVoiceOS/ovos-utils/pull/68) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.25a6](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.25a6) (2022-09-28) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.25a5...V0.0.25a6) + +**Merged pull requests:** + +- Add geolocate methods support in ovos\_api\_service [\#67](https://github.com/OpenVoiceOS/ovos-utils/pull/67) ([AIIX](https://github.com/AIIX)) + +## [V0.0.25a5](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.25a5) (2022-09-16) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.25a4...V0.0.25a5) + +**Implemented enhancements:** + +- Add methods for controlled notifications [\#66](https://github.com/OpenVoiceOS/ovos-utils/pull/66) ([AIIX](https://github.com/AIIX)) + +## [V0.0.25a4](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.25a4) (2022-09-10) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.25a3...V0.0.25a4) + +**Implemented enhancements:** + +- feat/timed\_lru\_cache [\#65](https://github.com/OpenVoiceOS/ovos-utils/pull/65) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.25a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.25a3) (2022-09-10) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.25a2...V0.0.25a3) + +**Fixed bugs:** + +- fix/syntax\_error [\#64](https://github.com/OpenVoiceOS/ovos-utils/pull/64) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.25a2](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.25a2) (2022-09-08) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.25a1...V0.0.25a2) + +**Merged pull requests:** + +- Add method to restart arbitrary systemd service [\#63](https://github.com/OpenVoiceOS/ovos-utils/pull/63) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.25a1](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.25a1) (2022-09-07) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.24...V0.0.25a1) + +**Implemented enhancements:** + +- add more api methods [\#62](https://github.com/OpenVoiceOS/ovos-utils/pull/62) ([AIIX](https://github.com/AIIX)) + +## [V0.0.24](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.24) (2022-09-07) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.24a4...V0.0.24) + +## [V0.0.24a4](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.24a4) (2022-09-06) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.24a3...V0.0.24a4) + +**Merged pull requests:** + +- add systemctl mycroft restart option [\#61](https://github.com/OpenVoiceOS/ovos-utils/pull/61) ([AIIX](https://github.com/AIIX)) + +## [V0.0.24a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.24a3) (2022-09-06) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.24a2...V0.0.24a3) + +**Implemented enhancements:** + +- feat/ovos\_api [\#60](https://github.com/OpenVoiceOS/ovos-utils/pull/60) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.24a2](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.24a2) (2022-08-17) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.24a1...V0.0.24a2) + +**Merged pull requests:** + +- Handle exceptions getting cache directory when MemoryTempfile fails \(i.e. in a chroot\) [\#58](https://github.com/OpenVoiceOS/ovos-utils/pull/58) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.24a1](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.24a1) (2022-08-15) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.23...V0.0.24a1) + +**Merged pull requests:** + +- Add extend about data method to gui utils [\#57](https://github.com/OpenVoiceOS/ovos-utils/pull/57) ([AIIX](https://github.com/AIIX)) + +## [V0.0.23](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.23) (2022-07-20) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.23a7...V0.0.23) + +## [V0.0.23a7](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.23a7) (2022-07-20) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.23a6...V0.0.23a7) + +**Implemented enhancements:** + +- Skill location utilities [\#55](https://github.com/OpenVoiceOS/ovos-utils/pull/55) ([NeonDaniel](https://github.com/NeonDaniel)) + +**Merged pull requests:** + +- Update release tag workflows to include version change commits [\#56](https://github.com/OpenVoiceOS/ovos-utils/pull/56) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.23a6](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.23a6) (2022-07-06) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.23a5...V0.0.23a6) + +**Merged pull requests:** + +- refactor/use ovos\_config package [\#52](https://github.com/OpenVoiceOS/ovos-utils/pull/52) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.23a5](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.23a5) (2022-07-06) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.23a4...V0.0.23a5) + +**Implemented enhancements:** + +- port/file\_watcher [\#54](https://github.com/OpenVoiceOS/ovos-utils/pull/54) ([NeonJarbas](https://github.com/NeonJarbas)) + +## [V0.0.23a4](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.23a4) (2022-07-06) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.23a3...V0.0.23a4) + +**Merged pull requests:** + +- Loosen mycroft-messagebus-client dependency to allow 0.10.0 [\#53](https://github.com/OpenVoiceOS/ovos-utils/pull/53) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.23a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.23a3) (2022-06-15) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.23a2...V0.0.23a3) + +**Fixed bugs:** + +- Prevent raising exception when msm config not present [\#51](https://github.com/OpenVoiceOS/ovos-utils/pull/51) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.23a2](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.23a2) (2022-06-10) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.23a1...V0.0.23a2) + +**Fixed bugs:** + +- fix/allow\_LF\_lang\_to\_be\_None [\#50](https://github.com/OpenVoiceOS/ovos-utils/pull/50) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.23a1](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.23a1) (2022-06-07) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.22...V0.0.23a1) + +**Fixed bugs:** + +- fix/screen\_check [\#49](https://github.com/OpenVoiceOS/ovos-utils/pull/49) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.22](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.22) (2022-06-02) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.22a3...V0.0.22) + +## [V0.0.22a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.22a3) (2022-06-02) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.22a2...V0.0.22a3) + +**Fixed bugs:** + +- Fix/full lang [\#48](https://github.com/OpenVoiceOS/ovos-utils/pull/48) ([NeonJarbas](https://github.com/NeonJarbas)) + +## [V0.0.22a2](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.22a2) (2022-05-31) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.22a1...V0.0.22a2) + +**Implemented enhancements:** + +- feat/lang\_utils [\#47](https://github.com/OpenVoiceOS/ovos-utils/pull/47) ([NeonJarbas](https://github.com/NeonJarbas)) + +## [V0.0.22a1](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.22a1) (2022-05-17) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.21...V0.0.22a1) + +## [V0.0.21](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.21) (2022-05-17) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.21a6...V0.0.21) + +## [V0.0.21a6](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.21a6) (2022-05-17) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.21a5...V0.0.21a6) + +## [V0.0.21a5](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.21a5) (2022-05-12) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.21a4...V0.0.21a5) + +## [V0.0.21a4](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.21a4) (2022-05-09) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.21a3...V0.0.21a4) + +## [V0.0.21a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.21a3) (2022-05-07) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.21a2...V0.0.21a3) + +## [V0.0.21a2](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.21a2) (2022-05-07) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.21a1...V0.0.21a2) + +## [V0.0.21a1](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.21a1) (2022-05-07) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.20...V0.0.21a1) + +## [V0.0.20](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.20) (2022-04-27) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.20a4...V0.0.20) + +## [V0.0.20a4](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.20a4) (2022-04-27) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.20a3...V0.0.20a4) + +## [V0.0.20a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.20a3) (2022-03-23) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.20a2...V0.0.20a3) + +## [V0.0.20a2](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.20a2) (2022-03-16) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.20a1...V0.0.20a2) + +## [V0.0.20a1](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.20a1) (2022-03-03) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.19...V0.0.20a1) + +## [V0.0.19](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.19) (2022-03-03) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.19a3...V0.0.19) + +## [V0.0.19a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.19a3) (2022-03-03) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.19a2...V0.0.19a3) + +## [V0.0.19a2](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.19a2) (2022-03-03) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.17a6...V0.0.19a2) + +## [V0.0.17a6](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.17a6) (2022-02-25) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.17a5...V0.0.17a6) + +## [V0.0.17a5](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.17a5) (2022-02-25) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.18...V0.0.17a5) + +## [V0.0.18](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.18) (2022-02-24) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.17a4...V0.0.18) + +## [V0.0.17a4](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.17a4) (2022-02-24) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/0.0.12...V0.0.17a4) + +## [0.0.12](https://github.com/OpenVoiceOS/ovos-utils/tree/0.0.12) (2021-11-04) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/25fe462e3c19a58f32dc1fd940bf7c96fc18e6de...0.0.12) + \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/ovos_utils/__init__.py b/ovos_utils/__init__.py index c2342305..9a850605 100644 --- a/ovos_utils/__init__.py +++ b/ovos_utils/__init__.py @@ -143,7 +143,7 @@ def wait_for_exit_signal(): try: Event().wait() except KeyboardInterrupt: - pass + LOG.debug(f"Exiting on KeyboardInterrupt") def get_handler_name(handler): diff --git a/ovos_utils/file_utils.py b/ovos_utils/file_utils.py index f64f5e17..b4faa4d4 100644 --- a/ovos_utils/file_utils.py +++ b/ovos_utils/file_utils.py @@ -3,6 +3,7 @@ import os import re import tempfile +from threading import RLock from typing import Optional, List import time @@ -72,17 +73,44 @@ def resolve_ovos_resource_file(res_name: str) -> Optional[str]: if os.path.isfile(res_name): return res_name - # now look in bundled ovos resources + # now look in bundled ovos-utils resources filename = join(dirname(__file__), "res", res_name) if os.path.isfile(filename): return filename + # let's look in ovos_workshop if it's installed + # (default skill resources live here) + try: + import ovos_workshop + core_root = dirname(ovos_workshop.__file__) + filename = join(core_root, "res", res_name) + if os.path.isfile(filename): + return filename + except: + pass + + # let's look in ovos_gui if it's installed + # (default GUI resources live here) + try: + import ovos_gui + core_root = dirname(ovos_gui.__file__) + filename = join(core_root, "res", res_name) + if os.path.isfile(filename): + return filename + except: + pass + # let's look in mycroft/ovos-core if it's installed - path = search_mycroft_core_location() - if path: - filename = join(path, "mycroft", "res", res_name) + # (default core resources live here / backwards compat) + try: + import mycroft + core_root = dirname(mycroft.__file__) + filename = join(core_root, "res", res_name) if os.path.isfile(filename): return filename + except: + pass + return None # Resource cannot be resolved @@ -288,37 +316,68 @@ def read_translated_file(filename: str, data: dict) -> Optional[List[str]]: class FileWatcher: - def __init__(self, files, callback, recursive=False, ignore_creation=False): + def __init__(self, files: List[str], callback: callable, + recursive: bool = False, ignore_creation: bool = False): + """ + Initialize a FileWatcher to monitor the specified files for changes + @param files: list of paths to monitor for file changes + @param callback: function to call on file change with modified file path + @param recursive: If true, recursively include directory contents + @param ignore_creation: If true, ignore file creation events + """ self.observer = Observer() self.handlers = [] for file_path in files: - watch_dir = dirname(file_path) - self.observer.schedule(FileEventHandler(file_path, callback, ignore_creation), + if os.path.isfile(file_path): + watch_dir = dirname(file_path) + else: + watch_dir = file_path + self.observer.schedule(FileEventHandler(file_path, callback, + ignore_creation), watch_dir, recursive=recursive) self.observer.start() def shutdown(self): + """ + Remove observer scheduled events and stop the observer. + """ self.observer.unschedule_all() self.observer.stop() class FileEventHandler(FileSystemEventHandler): - def __init__(self, file_path, callback, ignore_creation=False): + def __init__(self, file_path: str, callback: callable, + ignore_creation: bool = False): + """ + Create a handler for file change events + @param file_path: file_path being watched Unused(?) + @param callback: function to call on file change with modified file path + @param ignore_creation: if True, only track file modification events + """ super().__init__() self._callback = callback self._file_path = file_path - self._debounce = 1 - self._last_update = 0 if ignore_creation: self._events = ('modified') else: self._events = ('created', 'modified') + self._changed_files = [] + self._lock = RLock() def on_any_event(self, event): if event.is_directory: return - elif event.event_type in self._events: - if event.src_path == self._file_path: - if time.time() - self._last_update >= self._debounce: - self._callback(event.src_path) - self._last_update = time.time() + with self._lock: + if event.event_type == "closed": + if event.src_path in self._changed_files: + self._changed_files.remove(event.src_path) + # fire event, it is now safe + try: + self._callback(event.src_path) + except: + LOG.exception("An error occurred handling file " + "change event callback") + + elif event.event_type in self._events: + if event.src_path not in self._changed_files: + self._changed_files.append(event.src_path) diff --git a/ovos_utils/intents/intent_service_interface.py b/ovos_utils/intents/intent_service_interface.py index 9baa9cd0..38b330a2 100644 --- a/ovos_utils/intents/intent_service_interface.py +++ b/ovos_utils/intents/intent_service_interface.py @@ -250,8 +250,10 @@ def register_padatious_intent(self, intent_name, filename, lang): raise ValueError('Filename path must be a string') if not exists(filename): raise FileNotFoundError(f'Unable to find "{filename}"') - + with open(filename) as f: + samples = [_ for _ in f.read().split("\n") if _ and not _.startswith("#")] data = {'file_name': filename, + "samples": samples, 'name': intent_name, 'lang': lang} msg = dig_for_message() or Message("") @@ -271,11 +273,14 @@ def register_padatious_entity(self, entity_name, filename, lang): raise ValueError('Filename path must be a string') if not exists(filename): raise FileNotFoundError('Unable to find "{}"'.format(filename)) + with open(filename) as f: + samples = [_ for _ in f.read().split("\n") if _ and not _.startswith("#")] msg = dig_for_message() or Message("") if "skill_id" not in msg.context: msg.context["skill_id"] = self.skill_id self.bus.emit(msg.forward('padatious:register_entity', {'file_name': filename, + "samples": samples, 'name': entity_name, 'lang': lang})) diff --git a/ovos_utils/log.py b/ovos_utils/log.py index 83dcc171..c6f2a691 100644 --- a/ovos_utils/log.py +++ b/ovos_utils/log.py @@ -21,7 +21,13 @@ class LOG: """ Custom logger class that acts like logging.Logger - The logger name is automatically generated by the module of the caller + + The logger name is gnerally set by the calling module, but the default name + is read from the envvar `OVOS_DEFAULT_LOG_NAME`. + + The log level defaults to `INFO` and can be overridden by + `OVOS_DEFAULT_LOG_LEVEL`. Note that log level may be overridden by + configuration when calling `LOG.init`. Usage: >>> LOG.debug('My message: %s', debug_str) @@ -36,8 +42,8 @@ class LOG: formatter = logging.Formatter(fmt, datefmt) max_bytes = 50000000 backup_count = 3 - name = 'OVOS' - level = "DEBUG" + name = os.getenv("OVOS_DEFAULT_LOG_NAME") or 'OVOS' + level = os.getenv("OVOS_DEFAULT_LOG_LEVEL") or "INFO" diagnostic_mode = False _loggers = {} @@ -52,7 +58,8 @@ def init(cls, config=None): from ovos_config.meta import get_xdg_base default_base = get_xdg_base() except ImportError: - default_base = "mycroft" + default_base = os.environ.get("OVOS_CONFIG_BASE_FOLDER") or \ + "mycroft" from ovos_utils.xdg_utils import xdg_state_home config = config or {} @@ -60,7 +67,7 @@ def init(cls, config=None): f"{xdg_state_home()}/{default_base}" cls.max_bytes = config.get("max_bytes", 50000000) cls.backup_count = config.get("backup_count", 3) - cls.level = config.get("level", "INFO") + cls.level = config.get("level") or LOG.level cls.diagnostic_mode = config.get("diagnostic", False) @classmethod diff --git a/ovos_utils/messagebus.py b/ovos_utils/messagebus.py index c1339663..b35469b9 100644 --- a/ovos_utils/messagebus.py +++ b/ovos_utils/messagebus.py @@ -160,7 +160,7 @@ def __new__(cls, *args, **kwargs): from mycroft_bus_client import Message as _M return _M(*args, **kwargs) except: # FakeMessage - return super().__new__(cls, *args, **kwargs) + return super().__new__(cls) def __init__(self, msg_type, data=None, context=None): """Used to construct a message object diff --git a/ovos_utils/process_utils.py b/ovos_utils/process_utils.py index 7cda68a8..07538d55 100644 --- a/ovos_utils/process_utils.py +++ b/ovos_utils/process_utils.py @@ -348,8 +348,8 @@ def exists(self): with open(self.path, 'r') as L: try: os.kill(int(L.read()), SIGKILL) - except Exception as E: - pass + except Exception as e: + LOG.error(f"Failed to kill PID {L}: {e}") # # Create a lock file for this server process diff --git a/ovos_utils/skills/api.py b/ovos_utils/skills/api.py new file mode 100644 index 00000000..82cd7950 --- /dev/null +++ b/ovos_utils/skills/api.py @@ -0,0 +1,67 @@ +# Copyright 2020 Mycroft AI Inc. +# +# 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. +"""Skill Api + +The skill api allows skills interact with eachother over the message bus +just like interacting with any other object. +""" +from ovos_bus_client.message import Message + + +class SkillApi: + """SkillApi providing a simple interface to exported methods from skills + + Methods are built from a method_dict provided when initializing the skill. + """ + bus = None + + @classmethod + def connect_bus(cls, mycroft_bus): + """Registers the bus object to use.""" + cls.bus = mycroft_bus + + def __init__(self, method_dict): + self.method_dict = method_dict + for key in method_dict: + def get_method(k): + def method(*args, **kwargs): + m = self.method_dict[k] + data = {'args': args, 'kwargs': kwargs} + method_msg = Message(m['type'], data) + response = SkillApi.bus.wait_for_response(method_msg) + if (response and response.data and + 'result' in response.data): + return response.data['result'] + else: + return None + + return method + + self.__setattr__(key, get_method(key)) + + @staticmethod + def get(skill): + """Generate api object from skill id. + Args: + skill (str): skill id for target skill + + Returns: + SkillApi + """ + public_api_msg = '{}.public_api'.format(skill) + api = SkillApi.bus.wait_for_response(Message(public_api_msg)) + if api: + return SkillApi(api.data) + else: + return None diff --git a/ovos_utils/skills/locations.py b/ovos_utils/skills/locations.py index 1ec620e7..59f480ba 100644 --- a/ovos_utils/skills/locations.py +++ b/ovos_utils/skills/locations.py @@ -55,13 +55,13 @@ def get_skill_directories(conf: Optional[dict] = None) -> List[str]: # we are still dependent on the mycroft-core structure of skill_id/__init__.py conf = conf or read_mycroft_config() - + folder = conf["skills"].get("directory") or "skills" # load all valid XDG paths # NOTE: skills are actually code, but treated as user data! # they should be considered applets rather than full applications skill_locations = list(reversed( - [join(p, "skills") for p in get_xdg_data_dirs() if - isdir(join(p, "skills"))] + [join(p, folder) for p in get_xdg_data_dirs() if + isdir(join(p, folder))] )) # load the default skills folder @@ -103,6 +103,7 @@ def get_default_skills_directory(conf: Optional[dict] = None) -> str: """ conf = conf or read_mycroft_config() path_override = conf["skills"].get("directory_override") + folder = conf["skills"].get("directory") or "skills" # if .conf wants to use a specific path, use it! if path_override: @@ -114,12 +115,12 @@ def get_default_skills_directory(conf: Optional[dict] = None) -> str: len(conf["skills"].get("extra_directories")) > 0: skills_folder = expanduser(conf["skills"]["extra_directories"][0]) else: - skills_folder = join(get_xdg_data_save_path(), "skills") + skills_folder = join(get_xdg_data_save_path(), folder) # create folder if needed try: makedirs(skills_folder, exist_ok=True) except PermissionError: # old style /opt/mycroft/skills not available - skills_folder = join(get_xdg_data_save_path(), "skills") + skills_folder = join(get_xdg_data_save_path(), folder) makedirs(skills_folder, exist_ok=True) return skills_folder diff --git a/ovos_utils/time.py b/ovos_utils/time.py new file mode 100644 index 00000000..28d2649f --- /dev/null +++ b/ovos_utils/time.py @@ -0,0 +1,111 @@ +from datetime import datetime +from dateutil.tz import gettz, tzlocal +from typing import Any + +# used to calculate timespans +DAYS_IN_1_YEAR = 365.2425 +DAYS_IN_1_MONTH = 30.42 + + +def get_config_tz() -> Any: + """Get the configured timezone or, if missing, defaults to local timezone + + Returns: + Any: timezone + """ + try: + from ovos_config.locale import get_config_tz as _get_config_tz + return _get_config_tz() + except ImportError: + return tzlocal() + + +def now_utc() -> datetime: + """ Retrieve the current time in UTC + + Returns: + (datetime): The current time in Universal Time, aka GMT + """ + return datetime.utcnow().replace(tzinfo=gettz("UTC")) + + +def now_local(tz: datetime.tzinfo = None) -> datetime: + """ Retrieve the current time + + Args: + tz (datetime.tzinfo, optional): Timezone, default to user's settings + + Returns: + (datetime): The current time + """ + tz = tz or get_config_tz() + return datetime.now(tz) + + +def to_utc(dt: datetime) -> datetime: + """ Convert a datetime with timezone info to a UTC datetime + + Args: + dt (datetime): A datetime (presumably in some local zone) + Returns: + (datetime): time converted to UTC + """ + tz = gettz("UTC") + if not dt.tzinfo: + dt = dt.replace(tzinfo=get_config_tz()) + return dt.astimezone(tz) + + +def to_local(dt: datetime) -> datetime: + """ Convert a datetime to the user's local timezone + + Args: + dt (datetime): A datetime (if no timezone, defaults to UTC) + Returns: + (datetime): time converted to the local timezone + """ + tz = get_config_tz() + if not dt.tzinfo: + dt = dt.replace(tzinfo=get_config_tz()) + return dt.astimezone(tz) + + +def to_system(dt: datetime) -> datetime: + """Convert a datetime to the system's local timezone + + Args: + dt (datetime): A datetime (if no timezone, assumed to be UTC) + Returns: + (datetime): time converted to the operation system's timezone + """ + tz = tzlocal() + if not dt.tzinfo: + dt = dt.replace(tzinfo=get_config_tz()) + return dt.astimezone(tz) + + +def is_leap_year(year: int) -> bool: + """Checks if input year is a leap year + + Args: + year (int): the year to check + Returns: + bool: if the input is an leap year + """ + return (year % 400 == 0) or ((year % 4 == 0) and (year % 100 != 0)) + + +def get_next_leap_year(year: int) -> int: + """Get the following leap year of a reference year + + Args: + year (int): reference year + + Returns: + int: following leap year + """ + next_year = year + 1 + if is_leap_year(next_year): + return next_year + else: + return get_next_leap_year(next_year) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index de4f47f7..5f92a084 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -2,6 +2,6 @@ # START_VERSION_BLOCK VERSION_MAJOR = 0 VERSION_MINOR = 0 -VERSION_BUILD = 32 +VERSION_BUILD = 33 VERSION_ALPHA = 0 # END_VERSION_BLOCK diff --git a/readme.md b/readme.md index eb4b833c..fe8103d7 100644 --- a/readme.md +++ b/readme.md @@ -8,7 +8,3 @@ collection of simple utilities for use across the mycroft ecosystem pip install ovos_utils ``` -## Usage - -see [OVOS-CookBook](https://github.com/OpenVoiceOS/ovos_utils/wiki/OVOS-CookBook) - diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 44c870bc..0c05ebaf 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,6 +1,6 @@ -pexpect~=4.8 +pexpect~=4.6 requests~=2.26 json_database~=0.7 kthread~=0.2 watchdog -pyee \ No newline at end of file +pyee diff --git a/test/unittests/test_enclosure.py b/test/unittests/test_enclosure.py index 50f91aab..2296983d 100644 --- a/test/unittests/test_enclosure.py +++ b/test/unittests/test_enclosure.py @@ -1,6 +1,5 @@ import unittest -from mock import patch from ovos_utils.messagebus import FakeBus diff --git a/test/unittests/test_file_utils.py b/test/unittests/test_file_utils.py index 787ec25f..8a0efd84 100644 --- a/test/unittests/test_file_utils.py +++ b/test/unittests/test_file_utils.py @@ -1,5 +1,10 @@ +import shutil import unittest -from os.path import isdir, isfile +from os import makedirs +from os.path import isdir, join, dirname +from threading import Event +from time import time +from unittest.mock import Mock class TestFileUtils(unittest.TestCase): @@ -50,8 +55,133 @@ def test_read_translated_file(self): def test_filewatcher(self): from ovos_utils.file_utils import FileWatcher - # TODO + + test_dir = join(dirname(__file__), "test_watch") + test_file = join(test_dir, "test.watch") + makedirs(test_dir, exist_ok=True) + + # Test watch directory + called = Event() + callback = Mock(side_effect=lambda x: called.set()) + watcher = FileWatcher([test_dir], callback) + with open(test_file, 'w+') as f: + callback.assert_not_called() + + # Called on file close after creation + self.assertTrue(called.wait(3)) + callback.assert_called_once() + called.clear() + with open(test_file, 'w+') as f: + callback.assert_called_once() + # Called again on file close + self.assertTrue(called.wait(3)) + self.assertEqual(callback.call_count, 2) + + # Not called on directory creation + callback.reset_mock() + called.clear() + makedirs(join(test_dir, "new_dir")) + self.assertFalse(called.wait(3)) + callback.assert_not_called() + + # Not called on recursive file creation + with open(join(test_dir, "new_dir", "file.txt"), 'w+') as f: + callback.assert_not_called() + self.assertFalse(called.wait(3)) + callback.assert_not_called() + + watcher.shutdown() + + # Test recursive watch + called = Event() + callback = Mock(side_effect=lambda x: called.set()) + watcher = FileWatcher([test_dir], callback, recursive=True, + ignore_creation=True) + # Called on file change + with open(join(test_dir, "new_dir", "file.txt"), 'w+') as f: + callback.assert_not_called() + self.assertTrue(called.wait(3)) + callback.assert_called_once() + + # Not called on file creation + with open(join(test_dir, "new_dir", "new_file.txt"), 'w+') as f: + callback.assert_called_once() + self.assertTrue(called.wait(3)) + callback.assert_called_once() + + watcher.shutdown() + + # Test watch single file + called.clear() + callback = Mock(side_effect=lambda x: called.set()) + watcher = FileWatcher([test_file], callback) + with open(test_file, 'w+') as f: + callback.assert_not_called() + # Called on file close after change + self.assertTrue(called.wait(3)) + callback.assert_called_once() + watcher.shutdown() + + # Test changes on callback + contents = None + changed = Event() + + def _on_change(fp): + nonlocal contents + self.assertEqual(fp, test_file) + with open(fp, 'r') as f: + contents = f.read() + changed.set() + + watcher = FileWatcher([test_file], _on_change) + now_time = time() + with open(test_file, 'w') as f: + f.write(f"test {now_time}") + self.assertTrue(changed.wait(3)) + self.assertEqual(contents, f"test {now_time}") + watcher.shutdown() + + shutil.rmtree(test_dir) def test_file_event_handler(self): from ovos_utils.file_utils import FileEventHandler - # TODO + from watchdog.events import FileCreatedEvent, FileModifiedEvent, FileClosedEvent + test_file = join(dirname(__file__), "test.watch") + callback = Mock() + + # Test ignore creation callbacks + handler = FileEventHandler(test_file, callback, True) + handler.on_any_event(FileCreatedEvent(test_file)) + callback.assert_not_called() + + # Closed before modification (i.e. listener started while file open) + handler.on_any_event(FileClosedEvent(test_file)) + callback.assert_not_called() + + # Modified + handler.on_any_event(FileModifiedEvent(test_file)) + handler.on_any_event(FileModifiedEvent(test_file)) + callback.assert_not_called() + # Closed triggers callback + handler.on_any_event(FileClosedEvent(test_file)) + callback.assert_called_once() + # Second close won't trigger callback + handler.on_any_event(FileClosedEvent(test_file)) + callback.assert_called_once() + + # Test include creation callbacks + callback.reset_mock() + handler = FileEventHandler(test_file, callback, False) + handler.on_any_event(FileCreatedEvent(test_file)) + callback.assert_not_called() + + # Modified + handler.on_any_event(FileModifiedEvent(test_file)) + handler.on_any_event(FileModifiedEvent(test_file)) + callback.assert_not_called() + # Closed triggers callback + handler.on_any_event(FileClosedEvent(test_file)) + callback.assert_called_once() + # Second close won't trigger callback + handler.on_any_event(FileClosedEvent(test_file)) + callback.assert_called_once() diff --git a/test/unittests/test_gui.py b/test/unittests/test_gui.py index f57ae04b..8d650c2d 100644 --- a/test/unittests/test_gui.py +++ b/test/unittests/test_gui.py @@ -1,5 +1,5 @@ import unittest -from mock import patch, call +from unittest.mock import patch, call class TestGui(unittest.TestCase): diff --git a/test/unittests/test_intents.py b/test/unittests/test_intents.py index 59f5fbff..a090cc8b 100644 --- a/test/unittests/test_intents.py +++ b/test/unittests/test_intents.py @@ -1,8 +1,5 @@ import unittest -from mock import mock, patch -from ovos_utils.messagebus import FakeBus - class TestIntent(unittest.TestCase): from ovos_utils.intents import Intent diff --git a/test/unittests/test_log.py b/test/unittests/test_log.py index 4f2f3b32..baebb1a7 100644 --- a/test/unittests/test_log.py +++ b/test/unittests/test_log.py @@ -1,10 +1,63 @@ +import os +import shutil import unittest +import importlib + +from os.path import join, dirname, isdir, isfile class TestLog(unittest.TestCase): + test_dir = join(dirname(__file__), "log_test") + + @classmethod + def tearDownClass(cls) -> None: + if isdir(cls.test_dir): + shutil.rmtree(cls.test_dir) + def test_log(self): + import ovos_utils.log from ovos_utils.log import LOG - # TODO + # Default log config + self.assertEqual(LOG.base_path, "stdout") + self.assertIsInstance(LOG.fmt, str) + self.assertIsInstance(LOG.datefmt, str) + self.assertIsNotNone(LOG.formatter) + self.assertIsInstance(LOG.max_bytes, int) + self.assertIsInstance(LOG.backup_count, int) + self.assertEqual(LOG.name, "OVOS") + self.assertEqual(LOG.level, "INFO") + self.assertFalse(LOG.diagnostic_mode) + + # Override from envvars + os.environ["OVOS_DEFAULT_LOG_NAME"] = "test" + os.environ["OVOS_DEFAULT_LOG_LEVEL"] = "DEBUG" + importlib.reload(ovos_utils.log) + from ovos_utils.log import LOG + self.assertEqual(LOG.name, "test") + self.assertEqual(LOG.level, "DEBUG") + + # init log + test_config = {"path": self.test_dir, + "max_bytes": 100000, + "backup_count": 0, + "level": "WARNING", + "diagnostic": True} + LOG.init(test_config) + self.assertEqual(LOG.base_path, self.test_dir) + self.assertEqual(LOG.max_bytes, 100000) + self.assertEqual(LOG.backup_count, 0) + self.assertEqual(LOG.level, "WARNING") + self.assertTrue(LOG.diagnostic_mode) + + log_file = join(LOG.base_path, f"{LOG.name}.log") + self.assertFalse(isfile(log_file)) + LOG.info("This won't print") + self.assertTrue(isfile(log_file)) + LOG.warning("This will print") + with open(log_file) as f: + lines = f.readlines() + self.assertEqual(len(lines), 1) + self.assertTrue(lines[0].endswith("This will print\n")) def test_init_service_logger(self): from ovos_utils.log import init_service_logger diff --git a/test/unittests/test_skills.py b/test/unittests/test_skills.py index e6baa40b..66bea0c1 100644 --- a/test/unittests/test_skills.py +++ b/test/unittests/test_skills.py @@ -1,17 +1,28 @@ import unittest from os import environ -from os.path import isdir, join, dirname +from os.path import isdir, join, dirname, basename from unittest.mock import patch + +from ovos_utils.messagebus import FakeBus, Message from ovos_utils.skills.locations import get_skill_directories from ovos_utils.skills.locations import get_default_skills_directory from ovos_utils.skills.locations import get_installed_skill_ids from ovos_utils.skills.locations import get_plugin_skills + try: import ovos_config except ImportError: ovos_config = None +def _api_method_1(message: Message) -> str: + return message.serialize() + + +def _api_method_2(**kwargs) -> int: + return len(kwargs) + + class TestSkills(unittest.TestCase): def test_get_non_properties(self): from ovos_utils.skills import get_non_properties @@ -94,12 +105,30 @@ def test_get_skill_directories(self): self.assertEqual(get_skill_directories(config), [default_dir, extra_dir]) - # Define invalid directories in extra_directories config['skills']['extra_directories'] += ["/not/a/directory"] self.assertEqual(get_skill_directories(config), [default_dir, extra_dir]) + # Default directory + mock_config = {'skills': {}} + default_directories = get_skill_directories(mock_config) + for directory in default_directories: + self.assertEqual(basename(directory), 'skills') + # Configured directory + mock_config['skills']['directory'] = 'test' + test_directories = get_skill_directories(mock_config) + for directory in test_directories: + self.assertEqual(basename(directory), 'test') + self.assertEqual(len(default_directories), len(test_directories)) + # Extra directory + extra_dir = join(dirname(__file__), 'skills') + mock_config['skills']['extra_directories'] = [extra_dir] + extra_directories = get_skill_directories(mock_config) + self.assertEqual(extra_directories[-1], extra_dir) + for directory in test_directories: + self.assertIn(directory, extra_directories) + def test_get_default_skills_directory(self): if not ovos_config: return # skip test since ovos.conf isn't taken into account @@ -124,6 +153,21 @@ def test_get_default_skills_directory(self): config = {"skills": {"extra_directories": []}} self.assertEqual(get_default_skills_directory(config), xdg_skills_dir) + # Default directory + mock_config = {'skills': {}} + default_dir = get_default_skills_directory(mock_config) + self.assertTrue(isdir(default_dir)) + self.assertEqual(basename(default_dir), 'skills') + self.assertEqual(dirname(dirname(default_dir)), + join(dirname(__file__), "test_skills_xdg")) + # Override directory + mock_config['skills']['directory'] = 'test' + test_dir = get_default_skills_directory(mock_config) + self.assertTrue(isdir(test_dir)) + self.assertEqual(basename(test_dir), 'test') + self.assertEqual(dirname(dirname(test_dir)), + join(dirname(__file__), "test_skills_xdg")) + def test_get_plugin_skills(self): dirs, ids = get_plugin_skills() for d in dirs: @@ -131,3 +175,20 @@ def test_get_plugin_skills(self): for s in ids: self.assertIsInstance(s, str) self.assertEqual(len(dirs), len(ids)) + + +class TestSkillApi(unittest.TestCase): + bus = FakeBus() + + def test_skill_api_init(self): + from ovos_utils.skills.api import SkillApi + + test_api = SkillApi({"serialize": _api_method_1, + "get_length": _api_method_2}) + test_api.connect_bus(self.bus) + self.assertEqual(test_api.bus, self.bus) + self.assertEqual(SkillApi.bus, self.bus) + self.assertIsNotNone(test_api.serialize) + self.assertIsNotNone(test_api.get_length) + + # TODO: Test SkillApi.get diff --git a/test/unittests/test_time.py b/test/unittests/test_time.py new file mode 100644 index 00000000..2fc9275f --- /dev/null +++ b/test/unittests/test_time.py @@ -0,0 +1,100 @@ +import unittest +from os.path import expanduser +from dateutil.tz import tzlocal, tzfile, gettz +from datetime import datetime, timedelta, timezone +from mock import patch + +from ovos_utils.time import get_config_tz as _get_config_tz +from ovos_utils.time import ( + now_utc, + now_local, + to_local, + to_system, + to_utc, + is_leap_year, + get_next_leap_year +) + + +# South Africa has no DST, easier to work with +class TestTimeUtils(unittest.TestCase): + def test_get_tz_default_config(self): + timezone = _get_config_tz() + self.assertIsInstance(timezone, tzfile) + + @patch("ovos_config.locale.get_config_tz") + def test_get_tz_user_config(self, mock_tz): + mock_tz.return_value = gettz("Africa/Johannesburg") + self.assertIn("Johannesburg", _get_config_tz()._filename) + + def test_now_utc(self): + utc_now = now_utc() + self.assertIsInstance(utc_now, datetime) + self.assertEqual(utc_now.utcoffset().total_seconds(), 0) + + @patch("ovos_config.locale.get_config_tz") + def test_to_utc(self, mock_tz): + mock_tz.return_value = gettz("Africa/Johannesburg") + sa_now = datetime.now(gettz("Africa/Johannesburg")) + utc_now = to_utc(sa_now) + self.assertIsInstance(utc_now, datetime) + self.assertEqual(utc_now.utcoffset().total_seconds(), 0) + self.assertAlmostEqual(sa_now.utcoffset() - utc_now.utcoffset(), + timedelta(hours=2), + delta=timedelta(minutes=1)) + # w/o tz + utc_now = to_utc(sa_now.replace(tzinfo=None)) + self.assertAlmostEqual(sa_now.utcoffset() - utc_now.utcoffset(), + timedelta(hours=2), + delta=timedelta(minutes=1)) + + @patch("ovos_config.locale.get_config_tz") + def test_now_local(self, mock_tz): + mock_tz.return_value = gettz("Africa/Johannesburg") + local_now = now_local(gettz("Africa/Johannesburg")) + self.assertIsInstance(local_now, datetime) + self.assertAlmostEqual(local_now, + datetime.now(gettz("Africa/Johannesburg")), + delta=timedelta(minutes=1)) + # w/o tz + local_now = now_local() + self.assertAlmostEqual(local_now, + datetime.now(gettz("Africa/Johannesburg")), + delta=timedelta(minutes=1)) + + @patch("ovos_config.locale.get_config_tz") + def test_to_local(self, mock_tz): + mock_tz.return_value = gettz("UTC") + now_sa = datetime.now(gettz("Africa/Johannesburg")) + localized = to_local(now_sa) + self.assertIsInstance(localized, datetime) + self.assertAlmostEqual(now_sa.utcoffset() - localized.utcoffset(), + timedelta(hours=2), + delta=timedelta(minutes=1)) + + # w/o tzinfo + now_sa = datetime.now(gettz("Africa/Johannesburg")) + localized = to_local(now_sa.replace(tzinfo=None)) + self.assertIsInstance(localized, datetime) + self.assertEqual(localized.utcoffset().total_seconds(), 0) + + @patch("ovos_config.locale.get_config_tz") + def test_to_system(self, mock_tz): + mock_tz.return_value = gettz("Africa/Johannesburg") + now_sa = datetime.now(gettz("Africa/Johannesburg")) + now_sys = to_system(now_sa) + self.assertIsInstance(now_sys, datetime) + self.assertAlmostEqual(now_sys, datetime.now(tzlocal()), + delta=timedelta(minutes=1)) + # w/o tzinfo + now_sys = to_system(now_sa.replace(tzinfo=None)) + self.assertIsInstance(now_sys, datetime) + self.assertAlmostEqual(now_sys, datetime.now(tzlocal()), + delta=timedelta(minutes=1)) + + def test_is_leap_year(self): + self.assertEqual(is_leap_year(2001), False) + self.assertEqual(is_leap_year(2000), True) + + def test_next_leap_year(self): + self.assertEqual(get_next_leap_year(2001), 2004)