From abb49ae96786018c7a8a8cd3c8b30d612f710ed2 Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Sat, 10 Oct 2020 13:03:59 +0200 Subject: [PATCH 01/30] Set the next dev version number: 1.0.4-dev --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6674fd2..48359c6 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,10 @@ """Rachiopy setup script.""" from setuptools import find_packages, setup +from datetime import datetime -VERSION = "1.0.3" +NOW = datetime.now().strftime("%m%d%Y%H%M%S") + +VERSION = f"1.0.4-dev{NOW}" GITHUB_USERNAME = "rfverbruggen" GITHUB_REPOSITORY = "rachiopy" From 0f6f6b9f2b4e7d10a95aff010479069423f4386f Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Tue, 13 Oct 2020 08:07:54 +0200 Subject: [PATCH 02/30] Include python 3.9 in tests --- .github/workflows/publish.yml | 2 +- .github/workflows/publish_test.yml | 2 +- .github/workflows/test.yml | 2 +- tox.ini | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5dd5a33..6e9b268 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@master - name: Set up Python 3.8 - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: 3.8 - name: Install dependencies diff --git a/.github/workflows/publish_test.yml b/.github/workflows/publish_test.yml index 5cb070a..4034732 100644 --- a/.github/workflows/publish_test.yml +++ b/.github/workflows/publish_test.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@master - name: Set up Python 3.8 - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: 3.8 - name: Install dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9961237..c9d06c5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - python-version: [3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8, 3.9] # Steps represent a sequence of tasks that will be executed as part of the job steps: diff --git a/tox.ini b/tox.ini index c724886..6dd1052 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = clean, py36, py37, py38, pylint, flake8, pydocstyle, stats +envlist = clean, py36, py37, py38, py39, pylint, flake8, pydocstyle, stats ignore_basepython_conflict = true [testenv:clean] From fb0ed5df0fce6ff5a9807dcc25d865ffea5902ae Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Tue, 13 Oct 2020 08:10:26 +0200 Subject: [PATCH 03/30] Use a static python version when linting No need for matrix support over all python version when linting. --- .github/workflows/lint.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 002dfd5..e7abdf6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,18 +6,14 @@ jobs: # The type of runner that the job will run on runs-on: ubuntu-latest - strategy: - matrix: - python-version: [3.6, 3.7, 3.8] - # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python 3.8 uses: actions/setup-python@v2 with: - python-version: ${{ matrix.python-version }} + python-version: 3.8 - name: Install dependencies run: | python -m pip install --upgrade pip From f019fe6e418a9b47fce6c585cc0db3100c0a41a0 Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Tue, 13 Oct 2020 08:14:40 +0200 Subject: [PATCH 04/30] Fix lint.yml indent --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e7abdf6..db3bb99 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,7 +10,7 @@ jobs: steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.8 uses: actions/setup-python@v2 with: python-version: 3.8 From 82931fd82f95f48aa2db474af40d396a28b4c3c8 Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Tue, 13 Oct 2020 14:39:31 +0200 Subject: [PATCH 05/30] Update index.rst --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index bc1d1dd..ec30503 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,7 +19,7 @@ Getting Started from rachiopy import Rachio r = Rachio("8e600a4c-0027-4a9a-9bda-dc8d5c90350d") - resp, content = r.person.getInfo() + resp, content = r.person.info() print (resp["status"]) print (content["id"]) From 9f66c72a040a37900d6ecc2e48a08992928015de Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Tue, 13 Jul 2021 09:15:27 +0200 Subject: [PATCH 06/30] Include codeql-analysis --- .github/workflows/codeql-analysis.yml | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..72c6089 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ dev, master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ dev ] + schedule: + - cron: '28 6 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From 3a0bed68509db891d9062e1b031c82188c2d590b Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Thu, 20 Jan 2022 07:25:32 +0100 Subject: [PATCH 07/30] style: fixing some code smells --- tests/test_device.py | 32 ++++++++++++++++---------------- tests/test_flexschedulerule.py | 2 +- tests/test_notification.py | 6 +++--- tests/test_schedulerule.py | 2 +- tests/test_zone.py | 2 +- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/test_device.py b/tests/test_device.py index 53205c9..5f7b072 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -33,7 +33,7 @@ def test_get(self, mock): # Check that the mock function is called with the rights args. self.assertEqual( - args[1], f"{BASE_API_URL}/device/" f"{deviceid}", + args[1], f"{BASE_API_URL}/device/{deviceid}", ) self.assertEqual(args[0], "GET") self.assertEqual(kwargs["data"], None) @@ -49,9 +49,9 @@ def test_current_schedule(self, mock): args, kwargs = mock.call_args - # Check that the mock funciton is called with the rights args. + # Check that the mock function is called with the rights args. self.assertEqual( - args[1], f"{BASE_API_URL}/device/" f"{deviceid}/current_schedule", + args[1], f"{BASE_API_URL}/device/{deviceid}/current_schedule", ) self.assertEqual(args[0], "GET") self.assertEqual(kwargs["data"], None) @@ -69,7 +69,7 @@ def test_event(self, mock): args, kwargs = mock.call_args - # Check that the mock funciton is called with the rights args. + # Check that the mock function is called with the rights args. self.assertEqual( args[1], f"{BASE_API_URL}/device/" @@ -91,9 +91,9 @@ def test_forecast(self, mock): args, kwargs = mock.call_args - # Check that the mock funciton is called with the rights args. + # Check that the mock function is called with the rights args. self.assertEqual( - args[1], f"{BASE_API_URL}/device/" f"{deviceid}/forecast?units=US", + args[1], f"{BASE_API_URL}/device/{deviceid}/forecast?units=US", ) self.assertEqual(args[0], "GET") self.assertEqual(kwargs["data"], None) @@ -102,9 +102,9 @@ def test_forecast(self, mock): args, kwargs = mock.call_args - # Check that the mock funciton is called with the rights args. + # Check that the mock function is called with the rights args. self.assertEqual( - args[1], f"{BASE_API_URL}/device/" f"{deviceid}/forecast?units=US", + args[1], f"{BASE_API_URL}/device/{deviceid}/forecast?units=US", ) self.assertEqual(args[0], "GET") self.assertEqual(kwargs["data"], None) @@ -113,10 +113,10 @@ def test_forecast(self, mock): args, kwargs = mock.call_args - # Check that the mock funciton is called with the rights args. + # Check that the mock function is called with the rights args. self.assertEqual( args[1], - f"{BASE_API_URL}/device/" f"{deviceid}/forecast?units=METRIC", + f"{BASE_API_URL}/device/{deviceid}/forecast?units=METRIC", ) self.assertEqual(args[0], "GET") self.assertEqual(kwargs["data"], None) @@ -135,7 +135,7 @@ def test_stop_water(self, mock): args, kwargs = mock.call_args - # Check that the mock funciton is called with the rights args. + # Check that the mock function is called with the rights args. self.assertEqual( args[1], f"{BASE_API_URL}/device/stop_water", ) @@ -154,7 +154,7 @@ def test_rain_delay(self, mock): args, kwargs = mock.call_args - # Check that the mock funciton is called with the rights args. + # Check that the mock function is called with the rights args. self.assertEqual( args[1], f"{BASE_API_URL}/device/rain_delay", ) @@ -180,7 +180,7 @@ def test_turn_on(self, mock): args, kwargs = mock.call_args - # Check that the mock funciton is called with the rights args. + # Check that the mock function is called with the rights args. self.assertEqual( args[1], f"{BASE_API_URL}/device/on", ) @@ -198,7 +198,7 @@ def test_turn_off(self, mock): args, kwargs = mock.call_args - # Check that the mock funciton is called with the rights args. + # Check that the mock function is called with the rights args. self.assertEqual( args[1], f"{BASE_API_URL}/device/off", ) @@ -217,7 +217,7 @@ def test_pause_zone_run(self, mock): args, kwargs = mock.call_args - # Check that the mock funciton is called with the rights args. + # Check that the mock function is called with the rights args. self.assertEqual( args[1], f"{BASE_API_URL}/device/pause_zone_run", ) @@ -245,7 +245,7 @@ def test_resume_zone_run(self, mock): args, kwargs = mock.call_args - # Check that the mock funciton is called with the rights args. + # Check that the mock function is called with the rights args. self.assertEqual( args[1], f"{BASE_API_URL}/device/resume_zone_run", ) diff --git a/tests/test_flexschedulerule.py b/tests/test_flexschedulerule.py index ed1379e..71ec46f 100644 --- a/tests/test_flexschedulerule.py +++ b/tests/test_flexschedulerule.py @@ -32,7 +32,7 @@ def test_get(self, mock): # Check that the mock function is called with the rights args. self.assertEqual( args[1], - f"{BASE_API_URL}/flexschedulerule/" f"{flexscheduleruleid}", + f"{BASE_API_URL}/flexschedulerule/{flexscheduleruleid}", ) self.assertEqual(args[0], "GET") self.assertEqual(kwargs["data"], None) diff --git a/tests/test_notification.py b/tests/test_notification.py index d212aca..966d3f5 100644 --- a/tests/test_notification.py +++ b/tests/test_notification.py @@ -48,7 +48,7 @@ def test_get_device_webhook(self, mock): # Check that the mock function is called with the rights args. self.assertEqual( - args[1], f"{BASE_API_URL}/notification/" f"{deviceid}/webhook" + args[1], f"{BASE_API_URL}/notification/{deviceid}/webhook" ) self.assertEqual(args[0], "GET") self.assertEqual(kwargs["data"], None) @@ -124,7 +124,7 @@ def test_delete(self, mock): # Check that the mock function is called with the rights args. self.assertEqual( - args[1], f"{BASE_API_URL}/notification/webhook/" f"{hookid}" + args[1], f"{BASE_API_URL}/notification/webhook/{hookid}" ) self.assertEqual(args[0], "DELETE") self.assertEqual(kwargs["data"], None) @@ -142,7 +142,7 @@ def test_get(self, mock): # Check that the mock function is called with the rights args. self.assertEqual( - args[1], f"{BASE_API_URL}/notification/webhook/" f"{hookid}" + args[1], f"{BASE_API_URL}/notification/webhook/{hookid}" ) self.assertEqual(args[0], "GET") self.assertEqual(kwargs["data"], None) diff --git a/tests/test_schedulerule.py b/tests/test_schedulerule.py index b56a727..264d9a9 100644 --- a/tests/test_schedulerule.py +++ b/tests/test_schedulerule.py @@ -32,7 +32,7 @@ def test_get(self, mock): # Check that the mock function is called with the rights args. self.assertEqual( - args[1], f"{BASE_API_URL}/schedulerule/" f"{scheduleruleid}" + args[1], f"{BASE_API_URL}/schedulerule/{scheduleruleid}" ) self.assertEqual(args[0], "GET") self.assertEqual(kwargs["data"], None) diff --git a/tests/test_zone.py b/tests/test_zone.py index a634fa0..1ed8a0d 100644 --- a/tests/test_zone.py +++ b/tests/test_zone.py @@ -46,7 +46,7 @@ def test_get(self, mock): args, kwargs = mock.call_args # Check that the mock function is called with the rights args. - self.assertEqual(args[1], f"{BASE_API_URL}/zone/" f"{zoneid}") + self.assertEqual(args[1], f"{BASE_API_URL}/zone/{zoneid}") self.assertEqual(args[0], "GET") self.assertEqual(kwargs["data"], None) From e37f62422cf53886d1fc5c8de3323aa5d8739c39 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Wed, 22 Nov 2023 11:34:06 -0500 Subject: [PATCH 08/30] Update tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6dd1052..0bb7914 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = clean, py36, py37, py38, py39, pylint, flake8, pydocstyle, stats +envlist = clean, py37, py38, py39, pylint, flake8, pydocstyle, stats ignore_basepython_conflict = true [testenv:clean] From 6aa84b3f76ae895947d2046acef56632a8d1f132 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Wed, 22 Nov 2023 11:36:33 -0500 Subject: [PATCH 09/30] Update test.yml --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c9d06c5..7e46ba7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.7, 3.8, 3.9] # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -24,4 +24,4 @@ jobs: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi if [ -f requirements_test.txt ]; then pip install -r requirements_test.txt; fi - name: Run unittests - run: python -m unittest discover -v tests \ No newline at end of file + run: python -m unittest discover -v tests From 39b98c970eef18739af51bdaae4171a006314e76 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Sun, 10 Dec 2023 18:36:38 +0000 Subject: [PATCH 10/30] add valve support --- rachiopy/__init__.py | 6 ++ rachiopy/program.py | 86 ++++++++++++++++++++ rachiopy/rachioobject.py | 72 +++++++++++++++++ rachiopy/summary.py | 34 ++++++++ rachiopy/valve.py | 140 +++++++++++++++++++++++++++++++++ tests/constants.py | 1 + tests/test_program.py | 100 ++++++++++++++++++++++++ tests/test_rachio.py | 3 + tests/test_summary.py | 47 +++++++++++ tests/test_valve.py | 165 +++++++++++++++++++++++++++++++++++++++ 10 files changed, 654 insertions(+) create mode 100644 rachiopy/program.py create mode 100644 rachiopy/summary.py create mode 100644 rachiopy/valve.py create mode 100644 tests/test_program.py create mode 100644 tests/test_summary.py create mode 100644 tests/test_valve.py diff --git a/rachiopy/__init__.py b/rachiopy/__init__.py index e5fc760..2e3f311 100644 --- a/rachiopy/__init__.py +++ b/rachiopy/__init__.py @@ -7,6 +7,9 @@ from rachiopy.notification import Notification from rachiopy.schedulerule import Schedulerule from rachiopy.zone import Zone +from rachiopy.valve import Valve +from rachiopy.summary import SummaryServce +from rachiopy.program import Program class Rachio(RachioObject): @@ -25,3 +28,6 @@ def __init__(self, authtoken: str): self.notification = Notification(authtoken) self.schedulerule = Schedulerule(authtoken) self.zone = Zone(authtoken) + self.valve = Valve(authtoken) + self.summary = SummaryServce(authtoken) + self.program = Program(authtoken) diff --git a/rachiopy/program.py b/rachiopy/program.py new file mode 100644 index 0000000..ecdac26 --- /dev/null +++ b/rachiopy/program.py @@ -0,0 +1,86 @@ +"""Program module for the smart hose timer.""" + +from rachiopy.rachioobject import RachioObject + + +class Program(RachioObject): + """Program class for the smart hose timer.""" + + def list_programs(self, valve_id: str): + """Retreive the list of programs (schedules) for a valve.""" + + """For more info of the content in the response see: + https://rachio.readme.io/docs/programservice_listprograms + + :param valve_id: Valve's unique id + :type valve_id: str + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body. + :rtype: tuple + """ + path = f"program/listPrograms/{valve_id}" + return self.valve_get_request(path) + + def get_program(self, program_id: str): + """Retreive the information for a specific program. + + For more info of the content in the response see: + https://rachio.readme.io/docs/programservice_getprogram + + :param program_id: Program's unique id + :type program_id: str + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + path = f"program/getProgram/{program_id}" + return self.valve_get_request(path) + + def create_skip_overrides(self, program_id: str, timestamp: str): + """Create manual skips for the specific program run time. + You can retrieve the runtimes from SummaryService.getValveDayViews + + For more info of the content in the response see: + https://rachio.readme.io/docs/programservice_createskipoverrides + + :param program_id: Program's unique id + :type program_id: str + + :param timestamp: Timestamp of the run to skip + :type timestamp: timestamp + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + payload = {"programId": program_id, "timestamp": timestamp} + return self.valve_post_request("program/createSkipOverrides", payload) + + def delete_skip_overrides(self, program_id: str, timestamp: str): + """Cancel program skips for the specified program run time. + You can retrieve upcoming skips from SummaryService.getValveDayViews + + For more info of the content in the response see: + https://rachio.readme.io/docs/programservice_deleteskipoverrides + + :param program_id: Program's unique id + :type program_id: str + + :param timestamp: Timestamp of the run skip to delete + :type timestamp: timestamp + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + payload = {"programId": program_id, "timestamp": timestamp} + return self.valve_post_request("program/deleteSkipOverrides", payload) diff --git a/rachiopy/rachioobject.py b/rachiopy/rachioobject.py index f519908..11e4122 100644 --- a/rachiopy/rachioobject.py +++ b/rachiopy/rachioobject.py @@ -4,6 +4,7 @@ from requests import Session _API_URL = "https://api.rach.io/1/public" +_VALVE_URL = "https://cloud-rest.rach.io" class RachioObject: @@ -100,3 +101,74 @@ def delete_request(self, path: str, body=None): :rtype: tuple """ return self._request(path, "DELETE", body) + + def _valve_request(self, path: str, method: str, body=None): + """Make a request from the API. + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + + if body is not None: + body = json.dumps(body) + + url = f"{_VALVE_URL}/{path}" + response = self._http_session.request( + method, url, headers=self._headers, data=body, timeout=self.timeout + ) + + content_type = response.headers.get("content-type") + headers = {k.lower(): v for k, v in response.headers.items()} + headers["status"] = response.status_code + + if content_type and content_type.startswith("application/json"): + return headers, response.json() + + return headers, response.text + + def valve_get_request(self, path: str, body=None): + """Make a GET request to the valve API. + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + return self._valve_request(path, "GET", body) + + def valve_put_request(self, path: str, body=None): + """Make a PUT request to the valve API. + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + return self._valve_request(path, "PUT", body) + + def valve_post_request(self, path: str, body=None): + """Make a POST request to the valve API. + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + return self._valve_request(path, "POST", body) + + def valve_delete_request(self, path: str, body=None): + """Make a DELETE request to the valve API. + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + return self._valve_request(path, "DELETE", body) diff --git a/rachiopy/summary.py b/rachiopy/summary.py new file mode 100644 index 0000000..66805a6 --- /dev/null +++ b/rachiopy/summary.py @@ -0,0 +1,34 @@ +"""Smart Hose Timer scheudle summary calls.""" + +from rachiopy.rachioobject import RachioObject + + +class SummaryServce(RachioObject): + """Scheudle summary class.""" + + def get_valve_day_views(self, base_id: str, start, end): + """List historical and upcoming valve runs and skips. + + For more info of the content in the response see: + https://rachio.readme.io/docs/summaryservice_getvalvedayviews + + :param base_id: Base's unique id + :type dev_id: str + + :param start: Start date + :type start: Object[] + + :param end: End date + :type end: Object[] + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body. + :rtype: tuple + """ + payload = { + "resourceId": {"baseStationId": base_id}, + "start": start, + "end": end, + } + return self.valve_post_request("summary/getValveDayViews", payload) diff --git a/rachiopy/valve.py b/rachiopy/valve.py new file mode 100644 index 0000000..383999c --- /dev/null +++ b/rachiopy/valve.py @@ -0,0 +1,140 @@ +"""Valve Service.""" + +from rachiopy.rachioobject import RachioObject + + +class Valve(RachioObject): + """Valve class for smart hose timer.""" + + def get_base_station(self, base_id: str): + """Retreive the information for a specific base station. + + For more info of the content in the response see: + https://rachio.readme.io/docs/valveservice_getbasestation + + :param base_id: Base station's unique id + :type user_id: str + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + path = f"valve/getBaseStation/{base_id}" + return self.valve_get_request(path) + + def get_valve(self, valve_id: str): + """Retrieve the information for a specific smart valve. + + For more info of the content in the response see: + https://rachio.readme.io/docs/valveservice_getvalve + + :param valve_id: Valve's unique id + :type user_id: str + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + path = f"valve/getValve/{valve_id}" + return self.valve_get_request(path) + + def list_base_stations(self, user_id: str): + """Retrieve all base stations for a given user ID. + + For more info of the content in the response see: + https://rachio.readme.io/docs/valveservice_listbasestations + + :param user_id: Person's unique id + :type user_id: str + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + path = f"valve/listBaseStations/{user_id}" + return self.valve_get_request(path) + + def list_valves(self, base_id: str): + """Retreive all valves on a given base station. + + For more info of the content in the response see: + https://rachio.readme.io/docs/valveservice_listvalves + + :param base_id: Base station's unique id + :type user_id: str + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + path = f"valve/listValves/{base_id}" + return self.valve_get_request(path) + + def set_default_runtime(self, valve_id: str, duration: int): + """Set the runtime for a valve when the button is pressed. + + For more info of the content in the response see: + https://rachio.readme.io/docs/valveservice_setdefaultruntime + + :param valve_id: Valve's unique id + :type user_id: str + + :param duration: Duration in seconds + :type duration: int + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + payload = {"valveId": valve_id, "defaultRuntimeSeconds": duration} + return self.valve_put_request("valve/setDefaultRuntime", payload) + + def start_watering(self, valve_id: str, duration: int): + """Start a valve. + + For more info of the content in the response see: + https://rachio.readme.io/docs/valveservice_startwatering + + :param valve_id: Valve's unique id + :type user_id: str + + :param duration: Duration in seconds + :type duration: int + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + assert 0 <= duration <= 86400, "duration must be in range 0-86400" + payload = {"valveId": valve_id, "durationSeconds": duration} + return self.valve_put_request("valve/startWatering", payload) + + def stop_watering(self, valve_id: str): + """Stop a valve. + + For more info of the content in the response see: + https://rachio.readme.io/docs/valveservice_stopwatering + + :param valve_id: Valve's unique id + :type user_id: str + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + payload = {"valveId": valve_id} + return self.valve_put_request("valve/stopWatering", payload) diff --git a/tests/constants.py b/tests/constants.py index eec3442..c7b2a9c 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -5,6 +5,7 @@ from requests import Response BASE_API_URL = "https://api.rach.io/1/public" +VALVE_API_URL = "https://cloud-rest.rach.io" AUTHTOKEN = "1c1d9f3d-39c9-42b1-abc0-066f5a05cdef" diff --git a/tests/test_program.py b/tests/test_program.py new file mode 100644 index 0000000..7728689 --- /dev/null +++ b/tests/test_program.py @@ -0,0 +1,100 @@ +"""Program object test module""" + +import unittest +import uuid +import json +from unittest.mock import patch + +from rachiopy import Program +from tests.constants import VALVE_API_URL, AUTHTOKEN, RESPONSE200 + + +class TestProgramMethods(unittest.TestCase): + """Class containing the Program object tests.""" + + def setUp(self): + self.program = Program(AUTHTOKEN) + + def test_init(self): + """Test if the constructor works as expected.""" + self.assertEqual(self.program.authtoken, AUTHTOKEN) + + @patch("requests.Session.request") + def test_list_programs(self, mock): + """Test if the list programs method works as expected.""" + mock.return_value = RESPONSE200 + + valveid = str(uuid.uuid4()) + + self.program.list_programs(valveid) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual( + args[1], f"{VALVE_API_URL}/program/listPrograms/{valveid}" + ) + self.assertEqual(args[0], "GET") + self.assertEqual(kwargs["data"], None) + + @patch("requests.Session.request") + def test_get_program(self, mock): + """Test if the get program method works as expected.""" + mock.return_value = RESPONSE200 + + programid = str(uuid.uuid4()) + + self.program.get_program(programid) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual( + args[1], f"{VALVE_API_URL}/program/getProgram/{programid}" + ) + self.assertEqual(args[0], "GET") + self.assertEqual(kwargs["data"], None) + + @patch("requests.Session.request") + def test_create_skip_overrides(self, mock): + """Test if the create skip overrides method works as expected.""" + mock.return_value = RESPONSE200 + + programid = str(uuid.uuid4()) + timestamp = 1414818000000 + + self.program.create_skip_overrides(programid, timestamp) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual( + args[1], f"{VALVE_API_URL}/program/createSkipOverrides" + ) + self.assertEqual(args[0], "POST") + self.assertEqual( + kwargs["data"], + json.dumps({"programId": programid, "timestamp": timestamp}) + ) + + @patch("requests.Session.request") + def test_delete_skip_overrides(self, mock): + """Test if the delete skip overrides method works as expected.""" + mock.return_value = RESPONSE200 + + programid = str(uuid.uuid4()) + timestamp = 1414818000000 + + self.program.delete_skip_overrides(programid, timestamp) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual( + args[1], f"{VALVE_API_URL}/program/deleteSkipOverrides" + ) + self.assertEqual(args[0], "POST") + self.assertEqual( + kwargs["data"], + json.dumps({"programId": programid, "timestamp": timestamp}) + ) diff --git a/tests/test_rachio.py b/tests/test_rachio.py index cb6fac8..7140614 100644 --- a/tests/test_rachio.py +++ b/tests/test_rachio.py @@ -19,3 +19,6 @@ def test_init(self): self.assertEqual(rachio.schedulerule.authtoken, AUTHTOKEN) self.assertEqual(rachio.flexschedulerule.authtoken, AUTHTOKEN) self.assertEqual(rachio.notification.authtoken, AUTHTOKEN) + self.assertEqual(rachio.valve.authtoken, AUTHTOKEN) + self.assertEqual(rachio.summary.authtoken, AUTHTOKEN) + self.assertEqual(rachio.program.authtoken, AUTHTOKEN) diff --git a/tests/test_summary.py b/tests/test_summary.py new file mode 100644 index 0000000..979c359 --- /dev/null +++ b/tests/test_summary.py @@ -0,0 +1,47 @@ +"""Summary object test module""" + +import unittest +import uuid +import json +from unittest.mock import patch + +from rachiopy import SummaryServce +from tests.constants import VALVE_API_URL, AUTHTOKEN, RESPONSE200 + + +class TestSummaryMethod(unittest.TestCase): + """Class containing the Summary object test.""" + + def setUp(self): + self.summary = SummaryServce(AUTHTOKEN) + + def test_init(self): + """Test if the constructor works as expected.""" + self.assertEqual(self.summary.authtoken, AUTHTOKEN) + + @patch("requests.Session.request") + def test_get_valve_day_views(self, mock): + """Test if the get day views method works as expected.""" + mock.return_value = RESPONSE200 + + deviceid = str(uuid.uuid4()) + start = {"year": 2023, "month": 1, "day": 1} + end = {"year": 2023, "month": 1, "day": 30} + + self.summary.get_valve_day_views(deviceid, start, end) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual(args[1], f"{VALVE_API_URL}/summary/getValveDayViews") + self.assertEqual(args[0], "POST") + self.assertEqual( + kwargs["data"], + json.dumps( + { + "resourceId": {"baseStationId": deviceid}, + "start": start, + "end": end + } + ) + ) diff --git a/tests/test_valve.py b/tests/test_valve.py new file mode 100644 index 0000000..8f4a6c2 --- /dev/null +++ b/tests/test_valve.py @@ -0,0 +1,165 @@ +"""Valve object test module""" + +import unittest +from unittest.mock import patch +import uuid +import json + +from random import randrange +from rachiopy import Valve +from tests.constants import VALVE_API_URL, AUTHTOKEN, RESPONSE200, RESPONSE204 + + +class TestValveMethods(unittest.TestCase): + """Class containing the Valve object test cases.""" + + def setUp(self): + self.valve = Valve(AUTHTOKEN) + + def test_init(self): + """Test if the constructor works as expected.""" + self.assertEqual(self.valve.authtoken, AUTHTOKEN) + + @patch("requests.Session.request") + def test_get_valve(self, mock): + """Test if the get_valve method works as expected.""" + mock.return_value = RESPONSE200 + + valveid = uuid.uuid4() + + self.valve.get_valve(valveid) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual(args[1], f"{VALVE_API_URL}/valve/getValve/{valveid}") + self.assertEqual(args[0], "GET") + self.assertEqual(kwargs["data"], None) + + @patch("requests.Session.request") + def test_get_base_station(self, mock): + """Test if the get_base_station method works as expected.""" + mock.return_value = RESPONSE200 + + baseid = str(uuid.uuid4()) + + self.valve.get_base_station(baseid) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual( + args[1], + f"{VALVE_API_URL}/valve/getBaseStation/{baseid}" + ) + self.assertEqual(args[0], "GET") + self.assertEqual(kwargs["data"], None) + + @patch("requests.Session.request") + def test_list_base_stations(self, mock): + """Test if the list_base_stations method works as expected.""" + mock.return_value = RESPONSE200 + + userid = str(uuid.uuid4()) + + self.valve.list_base_stations(userid) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual( + args[1], + f"{VALVE_API_URL}/valve/listBaseStations/{userid}" + ) + self.assertEqual(args[0], "GET") + self.assertEqual(kwargs["data"], None) + + @patch("requests.Session.request") + def test_list_valves(self, mock): + """Test if the list_valves method works as expected.""" + mock.return_value = RESPONSE200 + + baseid = str(uuid.uuid4()) + + self.valve.list_valves(baseid) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual(args[1], f"{VALVE_API_URL}/valve/listValves/{baseid}") + self.assertEqual(args[0], "GET") + self.assertEqual(kwargs["data"], None) + + @patch("requests.Session.request") + def test_set_default_runtime(self, mock): + """Test if the set_default_runtime method works as expected.""" + mock.return_value = RESPONSE200 + + valveid = str(uuid.uuid4()) + duration = randrange(86400) + + self.valve.set_default_runtime(valveid, duration) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual(args[1], f"{VALVE_API_URL}/valve/setDefaultRuntime") + self.assertEqual(args[0], "PUT") + self.assertEqual( + kwargs["data"], + json.dumps({"valveId": valveid, "defaultRuntimeSeconds": duration}) + ) + + # Check that values should be within range. + self.assertRaises( + AssertionError, self.valve.start_watering, valveid, -1 + ) + self.assertRaises( + AssertionError, self.valve.start_watering, valveid, 86401 + ) + + @patch("requests.Session.request") + def test_start_watering(self, mock): + """Test if the start_watering method works as expected.""" + mock.return_value = RESPONSE204 + + valveid = str(uuid.uuid4()) + duration = randrange(86400) + + self.valve.start_watering(valveid, duration) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual(args[1], f"{VALVE_API_URL}/valve/startWatering") + self.assertEqual(args[0], "PUT") + self.assertEqual( + kwargs["data"], + json.dumps({"valveId": valveid, "durationSeconds": duration}) + ) + + # Check that values should be within range. + self.assertRaises( + AssertionError, self.valve.start_watering, valveid, -1 + ) + self.assertRaises( + AssertionError, self.valve.start_watering, valveid, 86401 + ) + + @patch("requests.Session.request") + def test_stop_watering(self, mock): + """Test if the stop_watering method works as expected.""" + mock.return_value = RESPONSE204 + + valveid = str(uuid.uuid4()) + + self.valve.stop_watering(valveid) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual(args[1], f"{VALVE_API_URL}/valve/stopWatering") + self.assertEqual(args[0], "PUT") + self.assertEqual( + kwargs["data"], json.dumps({"valveId": valveid}) + ) From 5a5cb347797f586de0133cb308c707bab4bcd24f Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Sun, 10 Dec 2023 18:42:37 +0000 Subject: [PATCH 11/30] fix program string --- rachiopy/program.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rachiopy/program.py b/rachiopy/program.py index ecdac26..6505195 100644 --- a/rachiopy/program.py +++ b/rachiopy/program.py @@ -7,9 +7,9 @@ class Program(RachioObject): """Program class for the smart hose timer.""" def list_programs(self, valve_id: str): - """Retreive the list of programs (schedules) for a valve.""" + """Retreive the list of programs (schedules) for a valve. - """For more info of the content in the response see: + For more info of the content in the response see: https://rachio.readme.io/docs/programservice_listprograms :param valve_id: Valve's unique id From b89cd3bccddb4565ec8eabfde6acdee4d6178508 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Sun, 10 Dec 2023 18:48:37 +0000 Subject: [PATCH 12/30] roll back to what worked try readd python 3.6 Revert "update formatting" This reverts commit 5b4ec911032d96a18590c60d2b75c6b444ad9fdb. update formatting --- .github/workflows/test.yml | 2 +- rachiopy/__init__.py | 1 + tox.ini | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7e46ba7..e6e71b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - python-version: [3.7, 3.8, 3.9] + python-version: [3.6, 3.7, 3.8, 3.9] # Steps represent a sequence of tasks that will be executed as part of the job steps: diff --git a/rachiopy/__init__.py b/rachiopy/__init__.py index 2e3f311..21d0910 100644 --- a/rachiopy/__init__.py +++ b/rachiopy/__init__.py @@ -15,6 +15,7 @@ class Rachio(RachioObject): """Object representing the Rachio API.""" + # pylint: disable=too-many-instance-attributes def __init__(self, authtoken: str): """Initialze the Rachio API wrapper. diff --git a/tox.ini b/tox.ini index 0bb7914..6dd1052 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = clean, py37, py38, py39, pylint, flake8, pydocstyle, stats +envlist = clean, py36, py37, py38, py39, pylint, flake8, pydocstyle, stats ignore_basepython_conflict = true [testenv:clean] From 02234d1a7817df6628fc3d177388815a847bc0dd Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Fri, 15 Dec 2023 15:44:38 +0000 Subject: [PATCH 13/30] remove python 3.6 --- .github/workflows/test.yml | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e6e71b0..7e46ba7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.7, 3.8, 3.9] # Steps represent a sequence of tasks that will be executed as part of the job steps: diff --git a/tox.ini b/tox.ini index 6dd1052..0bb7914 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = clean, py36, py37, py38, py39, pylint, flake8, pydocstyle, stats +envlist = clean, py37, py38, py39, pylint, flake8, pydocstyle, stats ignore_basepython_conflict = true [testenv:clean] From ef1429224e74960f777074f970b03f5645e96a0d Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Sun, 17 Dec 2023 14:55:37 +0000 Subject: [PATCH 14/30] try to make black happy --- tests/test_program.py | 12 ++++++------ tests/test_summary.py | 6 +++--- tests/test_valve.py | 28 +++++++++++++--------------- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/tests/test_program.py b/tests/test_program.py index 7728689..185e79d 100644 --- a/tests/test_program.py +++ b/tests/test_program.py @@ -33,7 +33,7 @@ def test_list_programs(self, mock): # Check that the mock function is called with the rights args. self.assertEqual( args[1], f"{VALVE_API_URL}/program/listPrograms/{valveid}" - ) + ) self.assertEqual(args[0], "GET") self.assertEqual(kwargs["data"], None) @@ -51,7 +51,7 @@ def test_get_program(self, mock): # Check that the mock function is called with the rights args. self.assertEqual( args[1], f"{VALVE_API_URL}/program/getProgram/{programid}" - ) + ) self.assertEqual(args[0], "GET") self.assertEqual(kwargs["data"], None) @@ -70,11 +70,11 @@ def test_create_skip_overrides(self, mock): # Check that the mock function is called with the rights args. self.assertEqual( args[1], f"{VALVE_API_URL}/program/createSkipOverrides" - ) + ) self.assertEqual(args[0], "POST") self.assertEqual( kwargs["data"], - json.dumps({"programId": programid, "timestamp": timestamp}) + json.dumps({"programId": programid, "timestamp": timestamp}), ) @patch("requests.Session.request") @@ -92,9 +92,9 @@ def test_delete_skip_overrides(self, mock): # Check that the mock function is called with the rights args. self.assertEqual( args[1], f"{VALVE_API_URL}/program/deleteSkipOverrides" - ) + ) self.assertEqual(args[0], "POST") self.assertEqual( kwargs["data"], - json.dumps({"programId": programid, "timestamp": timestamp}) + json.dumps({"programId": programid, "timestamp": timestamp}), ) diff --git a/tests/test_summary.py b/tests/test_summary.py index 979c359..651a0e7 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -41,7 +41,7 @@ def test_get_valve_day_views(self, mock): { "resourceId": {"baseStationId": deviceid}, "start": start, - "end": end + "end": end, } - ) - ) + ), + ) diff --git a/tests/test_valve.py b/tests/test_valve.py index 8f4a6c2..a4e5b65 100644 --- a/tests/test_valve.py +++ b/tests/test_valve.py @@ -49,9 +49,8 @@ def test_get_base_station(self, mock): # Check that the mock function is called with the rights args. self.assertEqual( - args[1], - f"{VALVE_API_URL}/valve/getBaseStation/{baseid}" - ) + args[1], f"{VALVE_API_URL}/valve/getBaseStation/{baseid}" + ) self.assertEqual(args[0], "GET") self.assertEqual(kwargs["data"], None) @@ -68,9 +67,8 @@ def test_list_base_stations(self, mock): # Check that the mock function is called with the rights args. self.assertEqual( - args[1], - f"{VALVE_API_URL}/valve/listBaseStations/{userid}" - ) + args[1], f"{VALVE_API_URL}/valve/listBaseStations/{userid}" + ) self.assertEqual(args[0], "GET") self.assertEqual(kwargs["data"], None) @@ -107,16 +105,18 @@ def test_set_default_runtime(self, mock): self.assertEqual(args[0], "PUT") self.assertEqual( kwargs["data"], - json.dumps({"valveId": valveid, "defaultRuntimeSeconds": duration}) + json.dumps( + {"valveId": valveid, "defaultRuntimeSeconds": duration} + ), ) # Check that values should be within range. self.assertRaises( AssertionError, self.valve.start_watering, valveid, -1 - ) + ) self.assertRaises( AssertionError, self.valve.start_watering, valveid, 86401 - ) + ) @patch("requests.Session.request") def test_start_watering(self, mock): @@ -135,16 +135,16 @@ def test_start_watering(self, mock): self.assertEqual(args[0], "PUT") self.assertEqual( kwargs["data"], - json.dumps({"valveId": valveid, "durationSeconds": duration}) + json.dumps({"valveId": valveid, "durationSeconds": duration}), ) # Check that values should be within range. self.assertRaises( AssertionError, self.valve.start_watering, valveid, -1 - ) + ) self.assertRaises( AssertionError, self.valve.start_watering, valveid, 86401 - ) + ) @patch("requests.Session.request") def test_stop_watering(self, mock): @@ -160,6 +160,4 @@ def test_stop_watering(self, mock): # Check that the mock function is called with the rights args. self.assertEqual(args[1], f"{VALVE_API_URL}/valve/stopWatering") self.assertEqual(args[0], "PUT") - self.assertEqual( - kwargs["data"], json.dumps({"valveId": valveid}) - ) + self.assertEqual(kwargs["data"], json.dumps({"valveId": valveid})) From 7d0e46c701dfbeade6dd8374f70f993d7ee30b81 Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Sat, 30 Dec 2023 09:09:34 +0100 Subject: [PATCH 15/30] build: update supported python versions --- .github/workflows/lint.yml | 6 +++--- .github/workflows/publish.yml | 4 ++-- .github/workflows/publish_test.yml | 6 +++--- .github/workflows/test.yml | 4 ++-- tox.ini | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index db3bb99..d17fa56 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,10 +10,10 @@ jobs: steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.x uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.x - name: Install dependencies run: | python -m pip install --upgrade pip @@ -24,4 +24,4 @@ jobs: - name: Run flake8 run: flake8 - name: Run pydocstyle - run: pydocstyle {posargs:rachiopy tests} \ No newline at end of file + run: pydocstyle {posargs:rachiopy tests} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6e9b268..c21bfc6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,10 +10,10 @@ jobs: runs-on: ubuntu-18.04 steps: - uses: actions/checkout@master - - name: Set up Python 3.8 + - name: Set up Python 3.x uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.x - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel diff --git a/.github/workflows/publish_test.yml b/.github/workflows/publish_test.yml index 4034732..60cee61 100644 --- a/.github/workflows/publish_test.yml +++ b/.github/workflows/publish_test.yml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-18.04 steps: - uses: actions/checkout@master - - name: Set up Python 3.8 + - name: Set up Python 3.x uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.x - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel @@ -28,4 +28,4 @@ jobs: uses: pypa/gh-action-pypi-publish@master with: password: ${{ secrets.test_pypi_password }} - repository_url: https://test.pypi.org/legacy/ \ No newline at end of file + repository_url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c9d06c5..106e572 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.8, 3.9, 3.10, 3.11, 3.12] # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -24,4 +24,4 @@ jobs: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi if [ -f requirements_test.txt ]; then pip install -r requirements_test.txt; fi - name: Run unittests - run: python -m unittest discover -v tests \ No newline at end of file + run: python -m unittest discover -v tests diff --git a/tox.ini b/tox.ini index 6dd1052..6980c70 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = clean, py36, py37, py38, py39, pylint, flake8, pydocstyle, stats +envlist = clean, py{38,39,310,311,312}, pylint, flake8, pydocstyle, stats ignore_basepython_conflict = true [testenv:clean] From de12e7925ab3edb7e45e8c88346109c4d56725a9 Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Sat, 30 Dec 2023 09:13:58 +0100 Subject: [PATCH 16/30] build: quote python test versions --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 106e572..bd440c5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - python-version: [3.8, 3.9, 3.10, 3.11, 3.12] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] # Steps represent a sequence of tasks that will be executed as part of the job steps: From cf018590e33392c680fe43ef101aef84b965e6fb Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Sat, 30 Dec 2023 09:32:53 +0100 Subject: [PATCH 17/30] chore: readd removed blank line --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e6de657..bd440c5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,6 +9,7 @@ jobs: strategy: matrix: python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it From 6ace17e0575bed84d995db06847dc571e04098dc Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Sat, 30 Dec 2023 11:24:01 +0100 Subject: [PATCH 18/30] docs: add all-contributors config --- README.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ddcf089..844c68b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -Rachiopy -======== +# Rachiopy + This python package provides a interface to the Rachio public API. -Usage ------ +## Usage + ```python from rachiopy import Rachio @@ -13,3 +13,14 @@ r.person.info() ``` For the complete documentation visit [read the docs](https://rachiopy.readthedocs.io/en/latest/). + +## Contributors + + + + + + + + + From 6995d43395219b398a406ea440c32ec7e9db1df7 Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Sat, 30 Dec 2023 11:24:11 +0100 Subject: [PATCH 19/30] docs: add all-contributors config --- .all-contributorsrc.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .all-contributorsrc.json diff --git a/.all-contributorsrc.json b/.all-contributorsrc.json new file mode 100644 index 0000000..f382d3f --- /dev/null +++ b/.all-contributorsrc.json @@ -0,0 +1,4 @@ +{ + "projectName": "rachiopy", + "projectOwner": "rfverbruggen" +} From e830e972915dd3e5f6dc630db67db10101b1e540 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 30 Dec 2023 10:31:26 +0000 Subject: [PATCH 20/30] docs: update README.md [skip ci] --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 844c68b..e68e8e6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # Rachiopy + +[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) + This python package provides a interface to the Rachio public API. @@ -19,6 +22,20 @@ For the complete documentation visit [read the docs](https://rachiopy.readthedoc + + + + + + +
Brian Rogers
Brian Rogers

đŸ’ģ 📖 ⚠ī¸
+ + + + + + + From 163b4239d46ada076a469b3a82cfc373adfeed13 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 30 Dec 2023 10:31:27 +0000 Subject: [PATCH 21/30] docs: create .all-contributorsrc [skip ci] --- .all-contributorsrc | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .all-contributorsrc diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 0000000..58ca321 --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,28 @@ +{ + "files": [ + "README.md" + ], + "imageSize": 100, + "commit": false, + "commitType": "docs", + "commitConvention": "angular", + "contributors": [ + { + "login": "brg468", + "name": "Brian Rogers", + "avatar_url": "https://avatars.githubusercontent.com/u/19143191?v=4", + "profile": "https://github.com/brg468", + "contributions": [ + "code", + "doc", + "test" + ] + } + ], + "contributorsPerLine": 7, + "skipCi": true, + "repoType": "github", + "repoHost": "https://github.com", + "projectName": "rachiopy", + "projectOwner": "rfverbruggen" +} From 04562d325a16664311cbd3b64988d212a803ef45 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Sat, 30 Dec 2023 20:33:55 +0000 Subject: [PATCH 22/30] update tests --- rachiopy/rachioobject.py | 4 ++-- tests/test_valve.py | 26 ++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/rachiopy/rachioobject.py b/rachiopy/rachioobject.py index 11e4122..4eec922 100644 --- a/rachiopy/rachioobject.py +++ b/rachiopy/rachioobject.py @@ -33,7 +33,7 @@ def __init__(self, authtoken: str, http_session=None, timeout=25): self.timeout = timeout def _request(self, path: str, method: str, body=None): - """Make a request from the API. + """Make a request to the API. :return: The return value is a tuple of (response, content), the first being and instance of the httplib2.Response class, the second @@ -103,7 +103,7 @@ def delete_request(self, path: str, body=None): return self._request(path, "DELETE", body) def _valve_request(self, path: str, method: str, body=None): - """Make a request from the API. + """Make a request to the API. :return: The return value is a tuple of (response, content), the first being and instance of the httplib2.Response class, the second diff --git a/tests/test_valve.py b/tests/test_valve.py index a4e5b65..d9ab8e7 100644 --- a/tests/test_valve.py +++ b/tests/test_valve.py @@ -110,12 +110,21 @@ def test_set_default_runtime(self, mock): ), ) + @patch("requests.Session.request") + def test_set_default_runtime_exception(self, mock): + """Test if the set_default_runtime method catches incorrect values.""" + mock.return_value = RESPONSE200 + + valveid = str(uuid.uuid4()) + duration1 = randrange(-50, -1) + duration2 = randrange(86401, 86500) + # Check that values should be within range. self.assertRaises( - AssertionError, self.valve.start_watering, valveid, -1 + AssertionError, self.valve.start_watering, valveid, duration1 ) self.assertRaises( - AssertionError, self.valve.start_watering, valveid, 86401 + AssertionError, self.valve.start_watering, valveid, duration2 ) @patch("requests.Session.request") @@ -138,12 +147,21 @@ def test_start_watering(self, mock): json.dumps({"valveId": valveid, "durationSeconds": duration}), ) + @patch("requests.Session.request") + def test_start_watering_exception(self, mock): + """Test if the start_watering method catches incorrect values.""" + mock.return_value = RESPONSE204 + + valveid = str(uuid.uuid4()) + duration1 = randrange(-50, -1) + duration2 = randrange(86401, 86500) + # Check that values should be within range. self.assertRaises( - AssertionError, self.valve.start_watering, valveid, -1 + AssertionError, self.valve.start_watering, valveid, duration1 ) self.assertRaises( - AssertionError, self.valve.start_watering, valveid, 86401 + AssertionError, self.valve.start_watering, valveid, duration2 ) @patch("requests.Session.request") From 31241eccb4b4fe33bdc66a75a1ff371846008729 Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Tue, 2 Jan 2024 18:54:55 +0100 Subject: [PATCH 23/30] Update publish.yml --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c21bfc6..b390915 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,7 +7,7 @@ on: jobs: build-n-publish: name: Build and publish Python distributions to PyPI - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@master - name: Set up Python 3.x From 4576f07334e49a5a18aa02f4f30d99a2aea6172a Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Tue, 2 Jan 2024 18:55:09 +0100 Subject: [PATCH 24/30] Update publish_test.yml --- .github/workflows/publish_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish_test.yml b/.github/workflows/publish_test.yml index 60cee61..d6fa9d0 100644 --- a/.github/workflows/publish_test.yml +++ b/.github/workflows/publish_test.yml @@ -8,7 +8,7 @@ on: jobs: build-n-publish: name: Build and publish Python distributions to Test PyPI - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@master - name: Set up Python 3.x From a9f6452b82df950e77bf783a47bdb1c428243434 Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Wed, 3 Jan 2024 10:03:38 +0100 Subject: [PATCH 25/30] Update publish_test.yml --- .github/workflows/publish_test.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish_test.yml b/.github/workflows/publish_test.yml index d6fa9d0..f3275c7 100644 --- a/.github/workflows/publish_test.yml +++ b/.github/workflows/publish_test.yml @@ -9,6 +9,11 @@ jobs: build-n-publish: name: Build and publish Python distributions to Test PyPI runs-on: ubuntu-latest + environment: + name: pypi + url: https://test.pypi.org/project/RachioPy + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - uses: actions/checkout@master - name: Set up Python 3.x @@ -24,8 +29,7 @@ jobs: run: python -m unittest discover -v tests - name: Build distribution run: python setup.py sdist bdist_wheel - - name: Publish distribution to Test PyPI - uses: pypa/gh-action-pypi-publish@master + - name: Publish package distributions to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 with: - password: ${{ secrets.test_pypi_password }} - repository_url: https://test.pypi.org/legacy/ + repository-url: https://test.pypi.org/legacy/ From 4ab25230a91abbb093cbbf8c6aced817bcfc3d7e Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Wed, 3 Jan 2024 10:08:00 +0100 Subject: [PATCH 26/30] Update setup.cfg --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index b88034e..306d97e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1 @@ [metadata] -description-file = README.md From a18f6aed0268b7688d9addd72415ce20af8d4f00 Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Wed, 3 Jan 2024 10:15:59 +0100 Subject: [PATCH 27/30] Update setup.py --- setup.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/setup.py b/setup.py index 48359c6..6a81f73 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,11 @@ PACKAGES = find_packages(exclude=["tests", "tests.*"]) +# read the contents of your README file +from pathlib import Path +this_directory = Path(__file__).parent +long_description = (this_directory / "README.md").read_text() + setup( name="RachioPy", version=VERSION, @@ -29,6 +34,8 @@ project_urls=PROJECT_URLS, license="MIT", description="A Python module for the Rachio API.", + long_description=long_description, + long_description_content_type='text/markdown', platforms="Cross Platform", classifiers=[ "Development Status :: 5 - Production/Stable", From e5a047c5f661dca376c8ce050184266ddd3bcffb Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Wed, 3 Jan 2024 10:16:14 +0100 Subject: [PATCH 28/30] Delete setup.cfg --- setup.cfg | 1 - 1 file changed, 1 deletion(-) delete mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 306d97e..0000000 --- a/setup.cfg +++ /dev/null @@ -1 +0,0 @@ -[metadata] From ab3eed256a9585206c07b137a2ccbfdadbc4d7dd Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Wed, 3 Jan 2024 10:19:02 +0100 Subject: [PATCH 29/30] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6a81f73..f176432 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ """Rachiopy setup script.""" from setuptools import find_packages, setup from datetime import datetime +from pathlib import Path NOW = datetime.now().strftime("%m%d%Y%H%M%S") @@ -18,7 +19,6 @@ PACKAGES = find_packages(exclude=["tests", "tests.*"]) # read the contents of your README file -from pathlib import Path this_directory = Path(__file__).parent long_description = (this_directory / "README.md").read_text() From 21005da930ad1433a50f216d0f210a3d7102fe0b Mon Sep 17 00:00:00 2001 From: Robbert Verbruggen Date: Wed, 3 Jan 2024 10:25:58 +0100 Subject: [PATCH 30/30] Update publish.yml --- .github/workflows/publish.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b390915..6836e35 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,6 +8,11 @@ jobs: build-n-publish: name: Build and publish Python distributions to PyPI runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/RachioPy + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - uses: actions/checkout@master - name: Set up Python 3.x @@ -25,12 +30,9 @@ jobs: run: python setup.py sdist bdist_wheel - name: Publish distribution to Test PyPI if: startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: - password: ${{ secrets.test_pypi_password }} - repository_url: https://test.pypi.org/legacy/ + repository-url: https://test.pypi.org/legacy/ - name: Publish distribution to PyPI if: startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@master - with: - password: ${{ secrets.pypi_password }} + uses: pypa/gh-action-pypi-publish@release/v1