From a91620b0b343063865d5e22748e8cefcb85a3e7e Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Tue, 12 May 2020 18:54:04 -0700 Subject: [PATCH 1/4] chore: update publish-docs script (via synth) (#14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR was generated using Autosynth. :rainbow:
Log from Synthtool ``` 2020-05-06 17:35:56,751 synthtool > Executing /tmpfs/src/github/synthtool/working_repo/synth.py. On branch autosynth nothing to commit, working tree clean .coveragerc .flake8 .github/CONTRIBUTING.md .github/ISSUE_TEMPLATE/bug_report.md .github/ISSUE_TEMPLATE/feature_request.md .github/ISSUE_TEMPLATE/support_request.md .github/PULL_REQUEST_TEMPLATE.md .github/release-please.yml .gitignore .kokoro/build.sh .kokoro/continuous/common.cfg .kokoro/continuous/continuous.cfg .kokoro/docs/common.cfg .kokoro/docs/docs.cfg .kokoro/presubmit/common.cfg .kokoro/presubmit/presubmit.cfg .kokoro/publish-docs.sh .kokoro/release.sh .kokoro/release/common.cfg .kokoro/release/release.cfg .kokoro/trampoline.sh CODE_OF_CONDUCT.md CONTRIBUTING.rst LICENSE MANIFEST.in docs/_static/custom.css docs/_templates/layout.html docs/conf.py.j2 docs/multiprocessing.rst noxfile.py.j2 renovate.json setup.cfg Running session blacken Creating virtual environment (virtualenv) using python3.6 in .nox/blacken pip install black==19.3b0 black docs google tests noxfile.py setup.py reformatted /tmpfs/src/github/synthtool/working_repo/docs/conf.py reformatted /tmpfs/src/github/synthtool/working_repo/noxfile.py All done! ✨ 🍰 ✨ 2 files reformatted, 15 files left unchanged. Session blacken was successful. 2020-05-06 17:36:00,462 synthtool > Wrote metadata to synth.metadata. ```
--- .kokoro/publish-docs.sh | 2 -- synth.metadata | 4 ++-- synth.py | 5 +++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.kokoro/publish-docs.sh b/.kokoro/publish-docs.sh index ad46909..7d5ecd5 100755 --- a/.kokoro/publish-docs.sh +++ b/.kokoro/publish-docs.sh @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -#!/bin/bash - set -eo pipefail # Disable buffering, so that the logs stream through. diff --git a/synth.metadata b/synth.metadata index 29562de..e7e28fa 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,14 +4,14 @@ "git": { "name": ".", "remote": "git@github.com:googleapis/python-runtimeconfig", - "sha": "fb3dc78a54500fe44713884e3119d5bfcfcff4d4" + "sha": "3256918855326b4975f9e5136b21e005c122f611" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "cdddf139b36000b3a7c65fd2a7781e253262359a" + "sha": "c585ac3b5eff5cd2097a5315ffd9cf4823cc1ed2" } } ] diff --git a/synth.py b/synth.py index f24bf78..60a18bd 100644 --- a/synth.py +++ b/synth.py @@ -25,6 +25,7 @@ # Add templated files # ---------------------------------------------------------------------------- templated_files = common.py_library(cov_level=97) -s.move(templated_files) +# this is an http library, not grpc +s.move(templated_files, excludes=["docs/multiprocessing.rst"]) -s.shell.run(["nox", "-s", "blacken"], hide_output=False) \ No newline at end of file +s.shell.run(["nox", "-s", "blacken"], hide_output=False) From 6b29562dd5ba712eb475139d5550c25374297e17 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Thu, 28 May 2020 15:47:55 -0700 Subject: [PATCH 2/4] chore: remove extra bash line (#16) --- .kokoro/release.sh | 2 -- synth.metadata | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.kokoro/release.sh b/.kokoro/release.sh index fd48b1c..5ed32f7 100755 --- a/.kokoro/release.sh +++ b/.kokoro/release.sh @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -#!/bin/bash - set -eo pipefail # Start the releasetool reporter diff --git a/synth.metadata b/synth.metadata index e7e28fa..1f9a472 100644 --- a/synth.metadata +++ b/synth.metadata @@ -3,15 +3,15 @@ { "git": { "name": ".", - "remote": "git@github.com:googleapis/python-runtimeconfig", - "sha": "3256918855326b4975f9e5136b21e005c122f611" + "remote": "https://github.com/googleapis/python-runtimeconfig.git", + "sha": "a91620b0b343063865d5e22748e8cefcb85a3e7e" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "c585ac3b5eff5cd2097a5315ffd9cf4823cc1ed2" + "sha": "274dd49554809834287c24b6dd324a85283f1182" } } ] From 84a50ad6cd0765bd86a4ed7c338aec2612e5e91c Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Fri, 5 Jun 2020 18:44:44 +0200 Subject: [PATCH 3/4] feat: support variable create / update methods and text attribute (#17) This PR adds support for variable create and update methods. The create method has been implemented in the Variable class instead of Config (which would have given symmetry with get_variable) for a couple of reasons: it allows a more natural workflow of calling config.variable(), setting the desired attribute, then saving it updates the created variable in place via _set_properties like other variable methods This also adds support for the text attribute, which can now be used in alternative to the previously supported value. The create and update methods enforce mutual exclusivity of the two attributes. Fixes #1 --- google/cloud/runtimeconfig/exceptions.py | 19 +++ google/cloud/runtimeconfig/variable.py | 111 +++++++++++++++- tests/unit/test_variable.py | 155 +++++++++++++++++++++++ 3 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 google/cloud/runtimeconfig/exceptions.py diff --git a/google/cloud/runtimeconfig/exceptions.py b/google/cloud/runtimeconfig/exceptions.py new file mode 100644 index 0000000..665b71e --- /dev/null +++ b/google/cloud/runtimeconfig/exceptions.py @@ -0,0 +1,19 @@ +# Copyright 2020 Google LLC +# +# 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. + +"""Exceptions used in the Google RuntimeConfig client.""" + + +class Error(Exception): + """Exception for all non-warning RuntimeConfig errors.""" diff --git a/google/cloud/runtimeconfig/variable.py b/google/cloud/runtimeconfig/variable.py index e7974c7..d68378b 100644 --- a/google/cloud/runtimeconfig/variable.py +++ b/google/cloud/runtimeconfig/variable.py @@ -41,8 +41,9 @@ import pytz from google.api_core import datetime_helpers -from google.cloud.exceptions import NotFound +from google.cloud.exceptions import Conflict, NotFound from google.cloud.runtimeconfig._helpers import variable_name_from_full_name +from google.cloud.runtimeconfig.exceptions import Error STATE_UNSPECIFIED = "VARIABLE_STATE_UNSPECIFIED" @@ -117,6 +118,34 @@ def client(self): """The client bound to this variable.""" return self.config.client + @property + def text(self): + """Text of the variable, as string. + + See + https://cloud.google.com/deployment-manager/runtime-configurator/reference/rest/v1beta1/projects.configs.variables + + :rtype: str or ``NoneType`` + :returns: The text of the variable or ``None`` if the property + is not set locally. + """ + return self._properties.get("text") + + @text.setter + def text(self, value): + """Set text property. + + If the variable is already using value, this will raise + exceptions.Error since text and value are mutually exclusive. + To persist the change, call create() or update(). + + :type value: str + :param value: The new value for the text property. + """ + if "value" in self._properties: + raise Error("Value and text are mutually exclusive.") + self._properties["text"] = value + @property def value(self): """Value of the variable, as bytes. @@ -133,6 +162,21 @@ def value(self): value = base64.b64decode(value) return value + @value.setter + def value(self, value): + """Set value property. + + If the variable is already using text, this will raise exceptions.Error + since text and value are mutually exclusive. + To persist the change, call create() or update(). + + :type value: bytes + :param value: The new value for the value property. + """ + if "text" in self._properties: + raise Error("Value and text are mutually exclusive.") + self._properties["value"] = value + @property def state(self): """Retrieve the state of the variable. @@ -204,6 +248,71 @@ def _set_properties(self, resource): self.name = variable_name_from_full_name(cleaned.pop("name")) self._properties.update(cleaned) + def _get_payload(self): + """Return the payload for create and update operations + + :rtype: dict + :returns: payload for API call with name and text or value attributes + """ + data = {"name": self.full_name} + if "text" in self._properties: + data["text"] = self._properties["text"] + elif "value" in self._properties: + value = self._properties["value"] + data["value"] = base64.b64encode(value).decode("utf-8") + else: + raise Error("No text or value set.") + return data + + def create(self, client=None): + """API call: create the variable via a POST request + + See + https://cloud.google.com/deployment-manager/runtime-configurator/reference/rest/v1beta1/projects.configs.variables/create + + :type client: :class:`~google.cloud.runtimeconfig.client.Client` + :param client: + (Optional) The client to use. If not passed, falls back to the + ``client`` stored on the variable's config. + + :rtype: bool + :returns: True if the variable has been created, False on error. + """ + client = self._require_client(client) + path = "%s/variables" % self.config.path + data = self._get_payload() + try: + resp = client._connection.api_request(method="POST", path=path, data=data) + except Conflict: + return False + self._set_properties(resp) + return True + + def update(self, client=None): + """API call: update the variable via a PUT request + + See + https://cloud.google.com/deployment-manager/runtime-configurator/reference/rest/v1beta1/projects.configs.variables/update + + :type client: :class:`~google.cloud.runtimeconfig.client.Client` + :param client: + (Optional) The client to use. If not passed, falls back to the + ``client`` stored on the variable's config. + + :rtype: bool + :returns: True if the variable has been created, False on error. + """ + client = self._require_client(client) + data = self._get_payload() + try: + resp = client._connection.api_request( + method="PUT", path=self.path, data=data + ) + except NotFound: + return False + self._set_properties(resp) + return True + def exists(self, client=None): """API call: test for the existence of the variable via a GET request diff --git a/tests/unit/test_variable.py b/tests/unit/test_variable.py index 3044a23..c4c3cd0 100644 --- a/tests/unit/test_variable.py +++ b/tests/unit/test_variable.py @@ -42,6 +42,11 @@ def _verifyResourceProperties(self, variable, resource): else: self.assertIsNone(variable.value) + if "text" in resource: + self.assertEqual(variable.text, resource["text"]) + else: + self.assertIsNone(variable.text) + if "state" in resource: self.assertEqual(variable.state, resource["state"]) @@ -112,6 +117,154 @@ def test_exists_hit_w_alternate_client(self): self.assertEqual(req["path"], "/%s" % (self.PATH,)) self.assertEqual(req["query_params"], {"fields": "name"}) + def test_create_no_data(self): + from google.cloud.runtimeconfig.config import Config + from google.cloud.runtimeconfig.exceptions import Error + + conn = _Connection() + client = _Client(project=self.PROJECT, connection=conn) + config = Config(name=self.CONFIG_NAME, client=client) + variable = config.variable(self.VARIABLE_NAME) + with self.assertRaises(Error) as ctx: + variable.create() + self.assertEqual("No text or value set.", str(ctx.exception)) + + def test_create_conflict(self): + from google.cloud.exceptions import Conflict + from google.cloud.runtimeconfig.config import Config + + conn = _Connection(Conflict("test")) + client = _Client(project=self.PROJECT, connection=conn) + config = Config(name=self.CONFIG_NAME, client=client) + variable = config.variable(self.VARIABLE_NAME) + variable.text = "foo" + self.assertFalse(variable.create()) + + def test_create_text(self): + from google.cloud.runtimeconfig.config import Config + + RESOURCE = { + "name": self.PATH, + "text": "foo", + "updateTime": "2016-04-14T21:21:54.5000Z", + "state": "UPDATED", + } + conn = _Connection(RESOURCE) + client = _Client(project=self.PROJECT, connection=conn) + config = Config(name=self.CONFIG_NAME, client=client) + variable = config.variable(self.VARIABLE_NAME) + variable.text = "foo" + result = variable.create() + self.assertTrue(result) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req["method"], "POST") + self.assertEqual( + req["path"], + "/projects/%s/configs/%s/variables" % (self.PROJECT, self.CONFIG_NAME), + ) + self._verifyResourceProperties(variable, RESOURCE) + + def test_create_value(self): + from google.cloud.runtimeconfig.config import Config + + RESOURCE = { + "name": self.PATH, + "value": "bXktdmFyaWFibGUtdmFsdWU=", # base64 my-variable-value + "updateTime": "2016-04-14T21:21:54.5000Z", + "state": "UPDATED", + } + conn = _Connection(RESOURCE) + client = _Client(project=self.PROJECT, connection=conn) + config = Config(name=self.CONFIG_NAME, client=client) + variable = config.variable(self.VARIABLE_NAME) + variable.value = b"my-variable-value" + result = variable.create() + self.assertTrue(result) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req["method"], "POST") + self.assertEqual( + req["path"], + "/projects/%s/configs/%s/variables" % (self.PROJECT, self.CONFIG_NAME), + ) + self._verifyResourceProperties(variable, RESOURCE) + + def test_update_text_conflict(self): + from google.cloud.runtimeconfig.config import Config + from google.cloud.runtimeconfig.exceptions import Error + + RESOURCE = { + "name": self.PATH, + "value": "bXktdmFyaWFibGUtdmFsdWU=", # base64 my-variable-value + "updateTime": "2016-04-14T21:21:54.5000Z", + "state": "UPDATED", + } + conn = _Connection(RESOURCE) + client = _Client(project=self.PROJECT, connection=conn) + config = Config(name=self.CONFIG_NAME, client=client) + variable = config.get_variable(self.VARIABLE_NAME) + with self.assertRaises(Error) as ctx: + variable.text = "bar" + self.assertEqual("Value and text are mutually exclusive.", str(ctx.exception)) + + def test_update_value_conflict(self): + from google.cloud.runtimeconfig.config import Config + from google.cloud.runtimeconfig.exceptions import Error + + RESOURCE = { + "name": self.PATH, + "text": "foo", + "updateTime": "2016-04-14T21:21:54.5000Z", + "state": "UPDATED", + } + conn = _Connection(RESOURCE) + client = _Client(project=self.PROJECT, connection=conn) + config = Config(name=self.CONFIG_NAME, client=client) + variable = config.get_variable(self.VARIABLE_NAME) + with self.assertRaises(Error) as ctx: + variable.value = b"bar" + self.assertEqual("Value and text are mutually exclusive.", str(ctx.exception)) + + def test_update_not_found(self): + from google.cloud.runtimeconfig.config import Config + + RESOURCE = { + "name": self.PATH, + "text": "foo", + "updateTime": "2016-04-14T21:21:54.5000Z", + "state": "UPDATED", + } + conn = _Connection(RESOURCE) + client = _Client(project=self.PROJECT, connection=conn) + config = Config(name=self.CONFIG_NAME, client=client) + variable = config.get_variable(self.VARIABLE_NAME) + self.assertFalse(variable.update()) + + def test_update_text(self): + from google.cloud.runtimeconfig.config import Config + + RESOURCE = { + "name": self.PATH, + "text": "foo", + "updateTime": "2016-04-14T21:21:54.5000Z", + "state": "UPDATED", + } + RESOURCE_UPD = RESOURCE.copy() + RESOURCE_UPD["text"] = "bar" + conn = _Connection(RESOURCE, RESOURCE_UPD) + client = _Client(project=self.PROJECT, connection=conn) + config = Config(name=self.CONFIG_NAME, client=client) + variable = config.get_variable(self.VARIABLE_NAME) + variable.text = "bar" + result = variable.update() + self.assertTrue(result) + self.assertEqual(len(conn._requested), 2) + req = conn._requested[1] + self.assertEqual(req["method"], "PUT") + self.assertEqual(req["path"], "/%s" % self.PATH) + self._verifyResourceProperties(variable, RESOURCE_UPD) + def test_reload_w_bound_client(self): from google.cloud.runtimeconfig.config import Config @@ -226,4 +379,6 @@ def api_request(self, **kw): except IndexError: raise NotFound("miss") else: + if issubclass(type(response), Exception): + raise response return response From 0a0c9f2906ca50ff95fa9686345f1b5779d7e46d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2020 13:44:12 -0700 Subject: [PATCH 4/4] chore: release 0.32.0 (#19) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f2c01a..d68ae99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/google-cloud-runtimeconfig/#history +## [0.32.0](https://www.github.com/googleapis/python-runtimeconfig/compare/v0.31.0...v0.32.0) (2020-06-05) + + +### Features + +* support variable create / update methods and text attribute ([#17](https://www.github.com/googleapis/python-runtimeconfig/issues/17)) ([84a50ad](https://www.github.com/googleapis/python-runtimeconfig/commit/84a50ad6cd0765bd86a4ed7c338aec2612e5e91c)), closes [#1](https://www.github.com/googleapis/python-runtimeconfig/issues/1) + ## [0.31.0](https://www.github.com/googleapis/python-runtimeconfig/compare/v0.30.0...v0.31.0) (2020-05-01) diff --git a/setup.py b/setup.py index d5d64a3..5c35d0c 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ name = "google-cloud-runtimeconfig" description = "Google Cloud RuntimeConfig API client library" -version = "0.31.0" +version = "0.32.0" # Should be one of: # 'Development Status :: 3 - Alpha' # 'Development Status :: 4 - Beta'