-
Notifications
You must be signed in to change notification settings - Fork 1
feat: implement check update hook #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
ec1c15e
Create check_update.py
WilliamBergamin 18fcc14
Add tests
WilliamBergamin 49633eb
add payload extraction
WilliamBergamin 5c28a78
Got the basic code working
WilliamBergamin 0dbc6cc
getting better
WilliamBergamin d13f851
Add unit test
WilliamBergamin e13fedf
Add unit tests
WilliamBergamin e96bd72
Improve type hinting
WilliamBergamin ea220aa
Improve script and tests
WilliamBergamin e4abc34
Make it work with python 3.6
WilliamBergamin 39f6e78
Improved hook
WilliamBergamin 672d56d
Update managed_os_env_vars.py
WilliamBergamin b1ecde2
fix linting issues
WilliamBergamin 5fbe2a5
Update slack_cli_hooks/hooks/check_update.py
WilliamBergamin 548a381
Update slack_cli_hooks/error/__init__.py
WilliamBergamin 980ce9d
Fix test
WilliamBergamin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,6 @@ | ||
| class CliError(Exception): | ||
| """General class for cli error""" | ||
|
|
||
|
|
||
| class PypiError(Exception): | ||
| """General class for PyPI package info retrieval error""" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| #!/usr/bin/env python | ||
| import json | ||
| from http.client import HTTPResponse | ||
| from types import ModuleType | ||
| from typing import Any, Dict, List, Optional | ||
| from urllib import request | ||
|
|
||
| import slack_bolt | ||
| import slack_sdk | ||
| from pkg_resources import parse_version as Version | ||
|
|
||
| import slack_cli_hooks.version | ||
| from slack_cli_hooks.error import PypiError | ||
| from slack_cli_hooks.protocol import Protocol, build_protocol | ||
|
|
||
| PROTOCOL: Protocol | ||
|
|
||
| DEPENDENCIES: List[ModuleType] = [slack_cli_hooks, slack_bolt, slack_sdk] | ||
|
|
||
|
|
||
| def parse_major(v: Version) -> int: | ||
| """The first item of :attr:`release` or ``0`` if unavailable. | ||
|
|
||
| >>> parse_major(Version("1.2.3")) | ||
| 1 | ||
| """ | ||
| # This implementation comes directly from the Version implementation since it is not supported in 3.6 | ||
| # source: https://github.com/pypa/packaging/blob/main/src/packaging/version.py | ||
| return v._version.release[0] if len(v._version) >= 1 else 0 # type: ignore | ||
|
|
||
|
|
||
| class Release: | ||
| def __init__( | ||
| self, | ||
| name: str, | ||
| current: Optional[Version] = None, | ||
| latest: Optional[Version] = None, | ||
| message: Optional[str] = None, | ||
| url: Optional[str] = None, | ||
| error: Optional[Dict[str, str]] = None, | ||
| ): | ||
| self.name = name | ||
| if current and latest: | ||
| self.current = current.base_version | ||
| self.latest = latest.base_version | ||
| self.update = current < latest | ||
| self.breaking = (parse_major(current) - parse_major(latest)) != 0 | ||
| if error: | ||
| self.error = error | ||
| if message: | ||
| self.message = message | ||
| if url: | ||
| self.url = url | ||
|
|
||
|
|
||
| def pypi_get(project: str, headers={"Accept": "application/json"}) -> HTTPResponse: | ||
| # Based on https://warehouse.pypa.io/api-reference/json.html | ||
| url = f"https://pypi.org/pypi/{project}/json" | ||
| pypi_request = request.Request(method="GET", url=url, headers=headers) | ||
| return request.urlopen(pypi_request) | ||
|
|
||
|
|
||
| def pypi_get_json(project: str) -> Dict[str, Any]: | ||
| pypi_response = pypi_get(project) | ||
| charset = pypi_response.headers.get_content_charset() or "utf-8" | ||
| raw_body = pypi_response.read().decode(charset) | ||
| if pypi_response.status > 200: | ||
| PROTOCOL.debug(f"Received status {pypi_response.status} from {pypi_response.url}") | ||
| PROTOCOL.debug(f"Headers {dict(pypi_response.getheaders())}") | ||
| PROTOCOL.debug(f"Body {raw_body}") | ||
| raise PypiError(f"Received status {pypi_response.status} from {pypi_response.url}") | ||
| return json.loads(raw_body) | ||
|
|
||
|
|
||
| def extract_latest_version(payload: Dict[str, Any]) -> str: | ||
| if "info" not in payload: | ||
| raise PypiError("Missing `info` field in pypi payload") | ||
| if "version" not in payload["info"]: | ||
| raise PypiError("Missing `version` field in pypi payload['info']") | ||
| return payload["info"]["version"] | ||
|
|
||
|
|
||
| def build_release(dependency: ModuleType) -> Release: | ||
| name = dependency.__name__ | ||
| try: | ||
| pypi_json_payload = pypi_get_json(name) | ||
| return Release( | ||
| name=name, | ||
| current=Version(dependency.version.__version__), | ||
| latest=Version(extract_latest_version(pypi_json_payload)), | ||
| ) | ||
| except PypiError as e: | ||
| return Release(name=name, error={"message": str(e)}) | ||
|
|
||
|
|
||
| def build_output(dependencies: List[ModuleType] = DEPENDENCIES) -> Dict[str, Any]: | ||
| output = {"name": "Slack Bolt", "url": "https://api.slack.com/automation/changelog", "releases": []} | ||
| errors = [] | ||
|
|
||
| for dep in dependencies: | ||
| release = build_release(dep) | ||
| output["releases"].append(vars(release)) | ||
|
|
||
| if hasattr(release, "error"): | ||
| errors.append(release.name) | ||
|
|
||
| if errors: | ||
| output["error"] = {"message": f"An error occurred fetching updates for the following packages: {', '.join(errors)}"} | ||
| return output | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| PROTOCOL = build_protocol() | ||
| PROTOCOL.respond(json.dumps(build_output())) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| from unittest.mock import patch | ||
| from urllib import request | ||
|
|
||
| from slack_cli_hooks.hooks import check_update | ||
| from slack_cli_hooks.hooks.check_update import build_output | ||
| from slack_cli_hooks.protocol.default_protocol import DefaultProtocol | ||
| from tests.utils import build_fake_dependency, build_fake_pypi_urlopen | ||
|
|
||
|
|
||
| class TestGetManifest: | ||
| def setup_method(self): | ||
| check_update.PROTOCOL = DefaultProtocol() | ||
|
|
||
| def test_build_output(self): | ||
| test_project = "test_proj" | ||
| fake_pypi_urlopen = build_fake_pypi_urlopen(status=200, body={"info": {"version": "0.0.1"}}) | ||
| test_dependency = build_fake_dependency(test_project, "0.0.0") | ||
|
|
||
| with patch.object(request, "urlopen") as mock_urlopen: | ||
| mock_urlopen.side_effect = fake_pypi_urlopen | ||
| actual = build_output([test_dependency]) | ||
|
|
||
| assert actual["name"] == "Slack Bolt" | ||
| assert len(actual["releases"]) == 1 | ||
| assert actual["releases"][0]["name"] == test_project | ||
| assert actual["releases"][0]["current"] == "0.0.0" | ||
| assert actual["releases"][0]["latest"] == "0.0.1" | ||
| assert actual["releases"][0]["update"] is True | ||
| assert actual["releases"][0]["breaking"] is False | ||
| assert "error" not in actual["releases"][0] | ||
|
|
||
| def test_build_output_error(self): | ||
| test_project = "test_proj" | ||
| fake_pypi_urlopen = build_fake_pypi_urlopen(status=200, body={"info": {}}) | ||
| test_dependency = build_fake_dependency(test_project, "0.0.0") | ||
|
|
||
| with patch.object(request, "urlopen") as mock_urlopen: | ||
| mock_urlopen.side_effect = fake_pypi_urlopen | ||
| actual = build_output([test_dependency]) | ||
|
|
||
| assert actual["name"] == "Slack Bolt" | ||
| assert len(actual["releases"]) == 1 | ||
| assert actual["releases"][0]["name"] == test_project | ||
| assert "error" in actual["releases"][0] | ||
| assert "message" in actual["releases"][0]["error"] | ||
| assert "error" in actual | ||
| assert "message" in actual["error"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| from unittest.mock import patch | ||
| from urllib import request | ||
|
|
||
| import pytest | ||
|
|
||
| from slack_cli_hooks.error import PypiError | ||
| from slack_cli_hooks.hooks import check_update | ||
| from slack_cli_hooks.hooks.check_update import ( | ||
| build_output, | ||
| build_release, | ||
| extract_latest_version, | ||
| pypi_get, | ||
| pypi_get_json, | ||
| ) | ||
| from slack_cli_hooks.protocol.default_protocol import DefaultProtocol | ||
| from tests.utils import build_fake_dependency, build_fake_pypi_urlopen | ||
|
|
||
|
|
||
| class TestGetManifest: | ||
| def setup_method(self): | ||
| check_update.PROTOCOL = DefaultProtocol() | ||
|
|
||
| def test_pypi_get(self): | ||
| test_project = "test_proj" | ||
| fake_pypi_urlopen = build_fake_pypi_urlopen() | ||
|
|
||
| with patch.object(request, "urlopen") as mock_urlopen: | ||
| mock_urlopen.side_effect = fake_pypi_urlopen | ||
| response = pypi_get(test_project) | ||
|
|
||
| assert response.url == f"https://pypi.org/pypi/{test_project}/json" | ||
| assert response.status == 200 | ||
| assert response.read().decode("utf-8") == "{}" | ||
|
|
||
| def test_pypi_get_json(self): | ||
| project = "my_test_project" | ||
| fake_pypi_urlopen = build_fake_pypi_urlopen(body={"info": {}, "releases": {}}) | ||
|
|
||
| with patch.object(request, "urlopen") as mock_urlopen: | ||
| mock_urlopen.side_effect = fake_pypi_urlopen | ||
| json_response = pypi_get_json(project) | ||
|
|
||
| assert json_response == {"info": {}, "releases": {}} | ||
|
|
||
| def test_pypi_get_json_fail(self): | ||
| project = "my_test_project" | ||
| fake_pypi_urlopen = build_fake_pypi_urlopen(status=300) | ||
|
|
||
| with patch.object(request, "urlopen") as mock_urlopen: | ||
| mock_urlopen.side_effect = fake_pypi_urlopen | ||
| with pytest.raises(PypiError) as e: | ||
| pypi_get_json(project) | ||
|
|
||
| assert "300" in str(e) | ||
| assert f"https://pypi.org/pypi/{project}/json" in str(e) | ||
|
|
||
| def test_extract_latest_version(self): | ||
| test_payload = {"info": {"version": "0.0.0"}} | ||
| actual = extract_latest_version(test_payload) | ||
| assert actual == "0.0.0" | ||
|
|
||
| def test_extract_latest_version_missing_info(self): | ||
| test_payload = {} | ||
| with pytest.raises(PypiError) as e: | ||
| extract_latest_version(test_payload) | ||
| assert "info" in str(e) | ||
|
|
||
| def test_extract_latest_version_missing_version(self): | ||
| test_payload = {"info": {}} | ||
| with pytest.raises(PypiError) as e: | ||
| extract_latest_version(test_payload) | ||
| assert "version" in str(e) | ||
| assert "payload['info']" in str(e) | ||
|
|
||
| def test_build_release(self): | ||
| test_project = "test-dependency" | ||
| test_dependency = build_fake_dependency(test_project, "0.0.0") | ||
|
|
||
| with patch.object(check_update, pypi_get_json.__name__) as mock_pypi_get_json: | ||
| mock_pypi_get_json.return_value = {"info": {"version": "0.0.1"}} | ||
| actual = build_release(test_dependency) | ||
|
|
||
| assert vars(actual) == { | ||
| "name": test_project, | ||
| "current": "0.0.0", | ||
| "latest": "0.0.1", | ||
| "update": True, | ||
| "breaking": False, | ||
| } | ||
|
|
||
| def test_build_release_error(self): | ||
| test_project = "test-dependency" | ||
| test_dependency = build_fake_dependency(test_project, "0.0.0") | ||
|
|
||
| with patch.object(check_update, pypi_get_json.__name__) as mock_pypi_get_json: | ||
| mock_pypi_get_json.return_value = {} | ||
| actual = build_release(test_dependency) | ||
|
|
||
| assert vars(actual) == { | ||
| "name": test_project, | ||
| "error": {"message": "Missing `info` field in pypi payload"}, | ||
| } | ||
|
|
||
| def test_build_output(self): | ||
| actual = build_output([]) | ||
|
|
||
| assert actual == { | ||
| "name": "Slack Bolt", | ||
| "url": "https://api.slack.com/automation/changelog", | ||
| "releases": [], | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.