diff --git a/.github/maintainers_guide.md b/.github/maintainers_guide.md
index 14b1f3d..b8ace89 100644
--- a/.github/maintainers_guide.md
+++ b/.github/maintainers_guide.md
@@ -20,14 +20,14 @@ Install necessary Python runtimes for development/testing. You can rely on GitHu
```zsh
pyenv install -l | grep -v "-e[conda|stackless|pypy]"
-pyenv install 3.8.5 # select the latest patch version
-pyenv local 3.8.5
+pyenv install 3.9.18 # select the latest patch version
+pyenv local 3.9.18
pyenv versions
system
3.6.10
3.7.7
-* 3.8.5 (set by /path-to-python-slack-hooks/.python-version)
+* 3.9.18 (set by /path-to-python-slack-hooks/.python-version)
pyenv rehash
```
@@ -35,8 +35,8 @@ pyenv rehash
Then, you can create a new Virtual Environment this way:
```zsh
-python -m venv env_3.8.5
-source env_3.8.5/bin/activate
+python -m venv env_3.9.18
+source env_3.9.18/bin/activate
```
## Tasks
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 671a84c..07528d5 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -12,7 +12,7 @@ jobs:
timeout-minutes: 5
strategy:
matrix:
- python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
+ python-version: ["3.9", "3.10", "3.11", "3.12", "pypy3.10"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
diff --git a/README.md b/README.md
index 6b3164c..de153df 100644
--- a/README.md
+++ b/README.md
@@ -1,154 +1,64 @@
Python Slack Hooks
-A helper library implementing the contract between the
-[Slack CLI][slack-cli-docs] and
-[Bolt for Python](https://slack.dev/bolt-python/)
+
+
+
+
+
+
-## Environment requirements
+This library defines the contract between the
+[Slack CLI](https://api.slack.com/automation/cli/install) and
+[Bolt for Python](https://slack.dev/bolt-python/).
-Before getting started, make sure you have a development workspace where you
-have permissions to install apps. **Please note that leveraging all features in
-this project require that the workspace be part of
-[a Slack paid plan](https://slack.com/pricing).**
+## Overview
-### Install the Slack CLI
+This library enables inter-process communication between the [Slack CLI](https://api.slack.com/automation/cli/install) and applications built with Bolt for Python.
-Install the Slack CLI. Step-by-step instructions can be found in this
-[Quickstart Guide][slack-cli-docs].
+When used together, the CLI delegates various tasks to the Bolt application by invoking processes ("hooks") and then making use of the responses provided by each hook's `stdout`.
-### Environment Setup
+For a complete list of available hooks, read the [Supported Hooks](#supported-hooks) section.
-Create a project folder and a
-[virtual environment](https://docs.python.org/3/library/venv.html#module-venv)
-within it
+## Requirements
-```zsh
-# Python 3.6+ required
-mkdir myproject
-cd myproject
-python3 -m venv .venv
-```
-
-Activate the environment
-
-```zsh
-source .venv/bin/activate
-```
-
-### Pypi
-
-Install this package using pip.
-
-```zsh
-pip install -U slack-cli-hooks
-```
+The latest minor version of [Bolt v1](https://pypi.org/project/slack-bolt/) is recommended.
-### Clone
+## Usage
-Clone this project using git.
+A Slack CLI-compatible Slack application includes a `./slack.json` file that contains hooks specific to that project. Each hook is associated with commands that are available in the Slack CLI. By default, `get-hooks` retrieves all of the [supported hooks](#supported-hooks) and their corresponding scripts as defined in this library.
-```zsh
-git clone https://github.com/slackapi/python-slack-hooks.git
-```
-
-Follow the
-[Develop Locally](https://github.com/slackapi/python-slack-hooks/blob/main/.github/maintainers_guide.md#develop-locally)
-steps in the maintainers guide to build and use this package.
+The CLI will always use the version of the `python-slack-hooks` that is specified in the project's `requirements.txt`.
-## Simple project
+### Supported Hooks
-In the same directory where we installed `slack-cli-hooks`
+The hooks currently supported for use within the Slack CLI include `check-update`, `get-hooks`, `get-manifest`, and `start`:
-1. Define basic information and metadata about our app via an
- [App Manifest](https://api.slack.com/reference/manifests) (`manifest.json`).
-2. Create a `slack.json` file that defines the interface between the
- [Slack CLI][slack-cli-docs] and [Bolt for Python][bolt-python-docs].
-3. Use an `app.py` file to define the entrypoint for a
- [Bolt for Python][bolt-python-docs] project.
+| Hook Name | CLI Command | File | Description |
+| --- | --- | --- | --- |
+| `check-update` | `slack update` | [check_update.py](./slack_cli_hooks/hooks/check_update.py) | Checks the project's Slack dependencies to determine whether or not any libraries need to be updated. |
+| `get-hooks` | All | [get_hooks.py](./slack_cli_hooks/hooks/get_hooks.py) | Fetches the list of available hooks for the CLI from this repository. |
+| `get-manifest` | `slack manifest` | [get_manifest.py](./slack_cli_hooks/hooks/get_manifest.py) | Converts a `manifest.json` file into a valid manifest JSON payload. |
+| `start` | `slack run` | [start.py](./slack_cli_hooks/hooks/start.py) | While developing locally, the CLI manages a socket connection with Slack's backend and utilizes this hook for events received via this connection. |
-### Application Configuration
+### Overriding Hooks
-Define your [Application Manifest](https://api.slack.com/reference/manifests) in
-a `manifest.json` file.
-
-```json
-{
- "display_information": {
- "name": "simple-app"
- },
- "outgoing_domains": [],
- "settings": {
- "org_deploy_enabled": true,
- "socket_mode_enabled": true,
- },
- "features": {
- "bot_user": {
- "display_name": "simple-app"
- }
- },
- "oauth_config": {
- "scopes": {
- "bot": ["chat:write"]
- }
- }
-}
-```
+To customize the behavior of a hook, add the hook to your application's `/slack.json` file, and provide a corresponding script to be executed.
-### CLI/Bolt Interface Configuration
+When commands are run, the Slack CLI will look to the project's hook definitions and use those instead of what's defined in this library, if provided.
-Define the Slack CLI configuration in a file named `slack.json`.
+Below is an example `/slack.json` file that overrides the default `start`:
```json
{
"hooks": {
- "get-hooks": "python3 -m slack_cli_hooks.hooks.get_hooks"
+ "get-hooks": "python3 -m slack_cli_hooks.hooks.get_hooks",
+ "start": "python3 app.py"
}
}
```
-### Source code
-
-Create a [Bolt for Python][bolt-python-docs] app in a file named `app.py`.
-Alternatively you can use an existing app instead.
-
-```python
-from slack_bolt import App
-from slack_bolt.adapter.socket_mode import SocketModeHandler
-
-app = App()
-
-# Add functionality here
-
-if __name__ == "__main__":
- SocketModeHandler(app).start()
-```
-
-## Running the app
-
-You should now be able to harness the power of the Slack CLI and Bolt.
-
-Run the app this way:
-
-```zsh
-slack run
-```
-
-## Getting Help
-
-If you get stuck we're here to help. Ensure your issue is related to this
-project and not to [Bolt for Python][bolt-python-docs]. The following are the
-best ways to get assistance working through your issue:
-
-- [Issue Tracker](https://github.com/slackapi/python-slack-hooks/issues) for
- questions, bug reports, feature requests, and general discussion. **Try
- searching for an existing issue before creating a new one.**
-- Email our developer support team: `support@slack.com`
-
## Contributing
-Contributions are more then welcome. Please look at the
+Contributions are always welcome! Please review the
[contributing guidelines](https://github.com/slackapi/python-slack-hooks/blob/main/.github/CONTRIBUTING.md)
-for more info!
-
-[slack-cli-docs]: https://api.slack.com/automation/cli
-[bolt-python-docs]: https://slack.dev/bolt-python/concepts
+for more information.
diff --git a/pyproject.toml b/pyproject.toml
index 4067b3d..6b1771a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,5 +1,5 @@
[build-system]
-requires = ["setuptools", "pytest-runner"]
+requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
@@ -8,7 +8,7 @@ dynamic = ["version", "readme", "dependencies"]
description = "The Slack CLI contract implementation for Bolt Python"
license = { text = "MIT" }
authors = [{ name = "Slack Technologies, LLC", email = "opensource@slack.com" }]
-requires-python = ">=3.6"
+requires-python = ">=3.9"
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"Environment :: Console",
@@ -18,9 +18,6 @@ classifiers = [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.6",
- "Programming Language :: Python :: 3.7",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
diff --git a/requirements/format.txt b/requirements/format.txt
index 8df1850..a0c3308 100644
--- a/requirements/format.txt
+++ b/requirements/format.txt
@@ -1,5 +1,4 @@
-black==22.8.0; python_version=="3.6"
-black; python_version>"3.6" # Until we drop Python 3.6 support, we have to stay with this version
+black
flake8>=5.0.4, <7;
pytype; (python_version<"3.11" or python_version>"3.11")
pytype==2023.11.29; python_version=="3.11"
diff --git a/scripts/_utils.sh b/scripts/_utils.sh
index f41d7aa..73ee0b6 100644
--- a/scripts/_utils.sh
+++ b/scripts/_utils.sh
@@ -10,16 +10,8 @@ clean_project() {
}
install_development_requirements() {
- python_version=`python --version | awk '{print $2}'`
-
- if [ ${python_version:0:3} == "3.6" ]
- then
- pip install -r requirements.txt
- else
- pip install -e .
- fi
-
pip install -U pip
+ pip install -e .
pip install -r requirements/testing.txt
pip install -r requirements/format.txt
}
diff --git a/slack_cli_hooks/error/__init__.py b/slack_cli_hooks/error/__init__.py
index 570528a..bd4efb1 100644
--- a/slack_cli_hooks/error/__init__.py
+++ b/slack_cli_hooks/error/__init__.py
@@ -1,2 +1,6 @@
class CliError(Exception):
"""General class for cli error"""
+
+
+class PypiError(Exception):
+ """General class for PyPI package info retrieval error"""
diff --git a/slack_cli_hooks/hooks/check_update.py b/slack_cli_hooks/hooks/check_update.py
new file mode 100644
index 0000000..61475f9
--- /dev/null
+++ b/slack_cli_hooks/hooks/check_update.py
@@ -0,0 +1,103 @@
+#!/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 packaging.version import 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]
+
+
+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 = (current.major - latest.major) != 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()))
diff --git a/slack_cli_hooks/hooks/get_hooks.py b/slack_cli_hooks/hooks/get_hooks.py
index df0a36a..1300a86 100644
--- a/slack_cli_hooks/hooks/get_hooks.py
+++ b/slack_cli_hooks/hooks/get_hooks.py
@@ -1,6 +1,11 @@
#!/usr/bin/env python
import json
-from slack_cli_hooks.protocol import Protocol, MessageBoundaryProtocol, DefaultProtocol, build_protocol
+from slack_cli_hooks.protocol import (
+ Protocol,
+ MessageBoundaryProtocol,
+ DefaultProtocol,
+ build_protocol,
+)
PROTOCOL: Protocol
EXEC = "python3"
@@ -10,6 +15,7 @@
"hooks": {
"get-manifest": f"{EXEC} -m slack_cli_hooks.hooks.get_manifest",
"start": f"{EXEC} -X dev -m slack_cli_hooks.hooks.start",
+ "check-update": f"{EXEC} -m slack_cli_hooks.hooks.check_update",
},
"config": {
"watch": {"filter-regex": "(^manifest\\.json$)", "paths": ["."]},
diff --git a/slack_cli_hooks/hooks/utils/managed_os_env_vars.py b/slack_cli_hooks/hooks/utils/managed_os_env_vars.py
index 8cd3f75..917da04 100644
--- a/slack_cli_hooks/hooks/utils/managed_os_env_vars.py
+++ b/slack_cli_hooks/hooks/utils/managed_os_env_vars.py
@@ -10,9 +10,7 @@ def __init__(self, protocol: Protocol) -> None:
def set_if_absent(self, os_env_var: str, value: str) -> None:
if os_env_var in os.environ:
- self._protocol.info(
- f"{os_env_var} environment variable detected in session, using it over the provided one!"
- )
+ self._protocol.info(f"{os_env_var} environment variable detected in session, using it over the provided one!")
return
self._os_env_vars.append(os_env_var)
os.environ[os_env_var] = value
diff --git a/slack_cli_hooks/version.py b/slack_cli_hooks/version.py
index e10a723..9d28a05 100644
--- a/slack_cli_hooks/version.py
+++ b/slack_cli_hooks/version.py
@@ -1,2 +1,2 @@
"""Check the latest version at https://pypi.org/project/slack-cli-hooks/"""
-__version__ = "0.0.0.dev0"
+__version__ = "0.0.0.dev2"
diff --git a/tests/scenario_test/test_check_update.py b/tests/scenario_test/test_check_update.py
new file mode 100644
index 0000000..9f64d2a
--- /dev/null
+++ b/tests/scenario_test/test_check_update.py
@@ -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"]
diff --git a/tests/slack_cli_hooks/hooks/test_check_update.py b/tests/slack_cli_hooks/hooks/test_check_update.py
new file mode 100644
index 0000000..af28dee
--- /dev/null
+++ b/tests/slack_cli_hooks/hooks/test_check_update.py
@@ -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": [],
+ }
diff --git a/tests/slack_cli_hooks/hooks/test_get_hooks.py b/tests/slack_cli_hooks/hooks/test_get_hooks.py
index 2486865..b1e77f5 100644
--- a/tests/slack_cli_hooks/hooks/test_get_hooks.py
+++ b/tests/slack_cli_hooks/hooks/test_get_hooks.py
@@ -8,6 +8,7 @@ def test_hooks_payload(self):
assert "slack_cli_hooks.hooks.get_manifest" in hooks["get-manifest"]
assert "slack_cli_hooks.hooks.start" in hooks["start"]
+ assert "slack_cli_hooks.hooks.check_update" in hooks["check-update"]
def test_hooks_payload_config(self):
config = hooks_payload["config"]
diff --git a/tests/utils.py b/tests/utils.py
index 185e41b..39ae6f1 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,4 +1,9 @@
+import json
import os
+from http.client import HTTPMessage, HTTPResponse
+from typing import Callable, Union
+from unittest.mock import MagicMock
+from urllib.request import Request
def remove_os_env_temporarily() -> dict:
@@ -9,3 +14,25 @@ def remove_os_env_temporarily() -> dict:
def restore_os_env(old_env: dict) -> None:
os.environ.update(old_env)
+
+
+def build_fake_pypi_urlopen(status: int = 200, headers=HTTPMessage(), body={}) -> Callable[..., HTTPResponse]:
+ headers.add_header("Content-Type", 'application/json; charset="UTF-8"')
+
+ mock_resp = HTTPResponse(MagicMock())
+ mock_resp.headers = headers
+ mock_resp.status = status
+ mock_resp.read = MagicMock(return_value=json.dumps(body).encode("UTF-8"))
+
+ def fake_urlopen(url: Union[str, Request]):
+ mock_resp.url = url.full_url if isinstance(url, Request) else url
+ return mock_resp
+
+ return fake_urlopen
+
+
+def build_fake_dependency(name: str, version: str):
+ fake_dependency = MagicMock()
+ fake_dependency.version.__version__ = version
+ fake_dependency.__name__ = name
+ return fake_dependency