diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 64ffe9c..9582311 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -9,6 +9,14 @@ name: python-ci on: [push] jobs: + + check-version-consistency: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - run: python .github/workflows/scripts/check-version-consistency.py + tests: runs-on: ubuntu-latest env: diff --git a/.github/workflows/scripts/check-version-consistency.py b/.github/workflows/scripts/check-version-consistency.py new file mode 100644 index 0000000..62ae22d --- /dev/null +++ b/.github/workflows/scripts/check-version-consistency.py @@ -0,0 +1,68 @@ +import os +import subprocess +import sys + + +PACKAGE_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) + + +class NotAVersionBranchException(Exception): + pass + + +class InvalidBranchNameFormat(Exception): + pass + + +def get_setup_version(): + process = subprocess.run(["python", "setup.py", "--version"], capture_output=True) + return process.stdout.strip().decode("utf8") + + +def get_branch_name(): + process = subprocess.run(["git", "branch", "--show-current"], capture_output=True) + return process.stdout.strip().decode("utf8") + + +def release_branch_version_matches_setup_version(setup_version, full_branch_name): + """ Check if the package version stated in setup.py matches the semantic version 'x.y.z' included in the branch name + of the format 'release/x.y.z'. + + :param str setup_version: + :param str full_branch_name: + :raise NotAVersionBranchException: + :return bool: + """ + try: + branch_type, branch_name = full_branch_name.split("/") + except ValueError: + raise InvalidBranchNameFormat( + f"The branch name must be in the form 'branch_type/branch_name'; received {full_branch_name!r}" + ) + + if branch_type != "release": + raise NotAVersionBranchException(f"The branch is not a release branch: {full_branch_name!r}.") + + return branch_name == setup_version + + +if __name__ == "__main__": + + os.chdir(PACKAGE_ROOT) + setup_version = get_setup_version() + full_branch_name = get_branch_name() + + try: + if release_branch_version_matches_setup_version(setup_version, full_branch_name): + print(f"Release branch name matches setup.py version: {setup_version!r}.") + sys.exit(0) + + print( + f"Release branch name does not match setup.py version: branch is {full_branch_name!r} but setup.py version " + f"is {setup_version!r}." + ) + sys.exit(1) + + except NotAVersionBranchException as e: + print(e.args[0]) + sys.exit(0) diff --git a/setup.py b/setup.py index 2b76b9d..868653f 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ setup( name="twined", - version="0.0.14", + version="0.0.15", py_modules=[], install_requires=["jsonschema ~= 3.2.0", "python-dotenv"], url="https://www.github.com/octue/twined", diff --git a/tests/test_children.py b/tests/test_children.py index 21a6287..ecacf56 100644 --- a/tests/test_children.py +++ b/tests/test_children.py @@ -37,13 +37,13 @@ def test_valid_children(self): "children": [{"key": "gis", "purpose": "The purpose.", "notes": "Some notes.", "filters": "tags:gis"}] } """ - self.assertEqual(len(Twine(source=source)._children), 1) + self.assertEqual(len(Twine(source=source).children), 1) def test_empty_children(self): """ Ensures that a twine file will validate with an empty list object as children """ twine = Twine(source="""{"children": []}""") - self.assertEqual(len(twine._children), 0) + self.assertEqual(len(twine.children), 0) class TestChildrenValidation(BaseTestCase): @@ -58,7 +58,15 @@ class TestChildrenValidation(BaseTestCase): VALID_CHILD_VALUE = """ [ - {"key": "gis", "id": "some-id", "uri_env_name": "NAME_OF_SOME_ENV_VAR_THAT_CONTAINS_A_URI"} + { + "key": "gis", + "id": "some-id", + "backend": { + "name": "GCPPubSubBackend", + "project_name": "my-project", + "credentials_filename": "hello.json" + } + } ] """ @@ -79,6 +87,13 @@ def test_extra_children(self): with self.assertRaises(exceptions.InvalidValuesContents): Twine().validate_children(source=self.VALID_CHILD_VALUE) + def test_backend_cannot_be_empty(self): + """ Test that the backend field of a child cannot be empty. """ + single_child_missing_backend = """[{"key": "gis", "id": "some-id", "backend": {}}]""" + + with self.assertRaises(exceptions.InvalidValuesContents): + Twine().validate_children(source=single_child_missing_backend) + def test_extra_key_validation_on_empty_twine(self): """ Test that children with extra data will not raise a validation error on an empty twine. """ @@ -101,7 +116,11 @@ def test_extra_key_validation_on_valid_twine(self): { "key": "gis", "id": "some-id", - "uri_env_name": "SOME_ENV_VAR_NAME", + "backend": { + "name": "GCPPubSubBackend", + "project_name": "my-project", + "credentials_filename": "hello.json" + }, "some_extra_property": "should not be a problem if present" } ] diff --git a/twined/schema/children_schema.json b/twined/schema/children_schema.json index 6f84192..4aa0368 100644 --- a/twined/schema/children_schema.json +++ b/twined/schema/children_schema.json @@ -8,15 +8,36 @@ "type": "string" }, "id": { - "description": "The universally unique ID of the running child twin", + "description": "The universally unique ID (UUID) of the running child twin", "type": "string" }, - "uri_env_name": { - "description": "Name of the environment variable containing the URI of the twin (which may contain credentials)", - "type": "string", - "pattern": "^[A-Z]+(?:_[A-Z]+)*$" + "backend": { + "description": "The backend running the child.", + "type": "object", + "oneOf": [ + { + "type": "object", + "title": "GCP Pub/Sub", + "properties": { + "name": { + "description": "Type of backend (in this case, it can only be GCPPubSubBackend)", + "type": "string", + "pattern": "^(GCPPubSubBackend)$" + }, + "project_name": { + "description": "Name of the Google Cloud Platform (GCP) project the child exists in.", + "type": "string" + }, + "credentials_filename": { + "description": "Absolute path to Google Cloud Platform credentials JSON file.", + "type": "string" + } + }, + "required": ["name", "project_name", "credentials_filename"] + } + ] } }, - "required": ["key", "id", "uri_env_name"] + "required": ["key", "id", "backend"] } } diff --git a/twined/twine.py b/twined/twine.py index 0ef56fe..2ac0d88 100644 --- a/twined/twine.py +++ b/twined/twine.py @@ -54,7 +54,7 @@ def __init__(self, **kwargs): """ Constructor for the twine class """ for name, strand in self._load_twine(**kwargs).items(): - setattr(self, "_" + name, strand) + setattr(self, name, strand) self._available_strands = tuple(trim_suffix(name, "_schema") for name in vars(self)) @@ -119,7 +119,7 @@ def _validate_against_schema(self, strand, data): else: if strand not in SCHEMA_STRANDS: raise exceptions.UnknownStrand(f"Unknown strand {strand}. Try one of {ALL_STRANDS}.") - schema_key = "_" + strand + "_schema" + schema_key = strand + "_schema" try: schema = getattr(self, schema_key) except AttributeError: @@ -178,14 +178,14 @@ def available_strands(self): """ return self._available_strands - def validate_children(self, **kwargs): + def validate_children(self, source, **kwargs): """ Validates that the children values, passed as either a file or a json string, are correct """ # TODO cache this loaded data keyed on a hashed version of kwargs - children = self._load_json("children", **kwargs) + children = self._load_json("children", source, **kwargs) self._validate_against_schema("children", children) - strand = getattr(self, "_children", []) + strand = getattr(self, "children", []) # Loop the children and accumulate values so we have an O(1) check children_keys = {} @@ -249,7 +249,7 @@ def validate_credentials(self, dotenv_path=None): # Loop through the required credentials to check for presence of each credentials = {} - for credential in getattr(self, "_credentials", []): + for credential in getattr(self, "credentials", []): name = credential["name"] default = credential.get("default", None) credentials[name] = os.environ.get(name, default)