diff --git a/.gitignore b/.gitignore index fe63595..028b49a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ .gitignore -pyproject.toml .idea/ __pycache__/ dist/ easier_docker.egg-info/ +example.egg-info/ venv/ build/ -.coverage \ No newline at end of file +.coverage +htmlcov +easierdocker/easier_docker.egg-info/ diff --git a/Makefile b/Makefile index 6a137c0..03c3e19 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,31 @@ -.PHONY: test clean all +.PHONY: test clean build upload all -all: test clean +TWINE_UPLOAD := twine upload --repository pypi --username __token__ --password $(TWINE_API_TOKEN) + +all: clean build test: coverage run -m unittest discover coverage report + coverage html + google-chrome htmlcov/index.html + clean: find . -name '__pycache__' -type d -exec rm -rf {} + - rm -rf build/* - rm -rf dist/* - rm -rf dist/* - rm -rf easier_docker.egg-info/* + find . -name 'easier_docker.egg-info' -type d -exec rm -rf {} + + rm -rf build + rm -rf dist rm -rf .coverage + rm -rf htmlcov + +build: + python -m build + +upload: + @echo "Uploading the package..." + @if [ -z "$(TWINE_API_TOKEN)" ]; then \ + echo "Error: TWINE_API_TOKEN is not set. Please export it as an environment variable."; \ + exit 1; \ + fi + $(TWINE_UPLOAD) dist/* diff --git a/README.md b/README.md index 3934b92..fc08079 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ -

easier-docker

+# easier-docker -

- - - - -

+![PyPI - Python Version](https://img.shields.io/pypi/pyversions/easier-docker) +![Static Badge](https://img.shields.io/badge/-docker-grey?logo=docker) +![GitHub License](https://img.shields.io/github/license/touero/easier-docker) +![PyPI - License](https://img.shields.io/pypi/l/easier-docker) +![PyPI - Downloads](https://img.shields.io/pypi/dm/easier-docker) +![GitHub last commit](https://img.shields.io/github/last-commit/touero/easier-docker) +[![Upload Package To PyPI](https://github.com/touero/easier-docker/actions/workflows/python-publish.yml/badge.svg?branch=master)](https://github.com/touero/easier-docker/actions/workflows/python-publish.yml) ## Repository Introduction @@ -23,13 +24,13 @@ Please check [wiki](https://github.com/touero/easier-docker/wiki). ## Related ### Repository -- [docker-py](https://github.com/docker/docker-py) — A Python library for the Docker Engine API. +[docker-py](https://github.com/docker/docker-py) — A Python library for the Docker Engine API. ### Materials -- [Docker SDK for Python](https://docker-py.readthedocs.io/en/stable/) +[Docker SDK for Python](https://docker-py.readthedocs.io/en/stable/) -### Repository Used -- [opsariichthys-bidens](https://github.com/weiensong/opsariichthys-bidens) — About +### Repository Used Example +[opsariichthys-bidens](https://github.com/weiensong/opsariichthys-bidens) — About Building a Basic Information API for Chinese National Universities in the Handheld College Entrance Examination Based on Fastapi. @@ -38,7 +39,8 @@ Building a Basic Information API for Chinese National Universities in the Handhe ## Contributing -[Open an issue](https://github.com/weiensong/easier_docker/issues) or submit PRs. +[Open an issue](https://github.com/weiensong/easier_docker/issues) or submit PRs to git branch `develop`. + Standard Python follows the [Python PEP-8](https://peps.python.org/pep-0008/) Code of Conduct. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..733006e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,37 @@ +[build-system] +requires = ["setuptools>=68.2.0", "wheel>=0.42.0"] +build-backend = "setuptools.build_meta" + + +[tool.setuptools.packages] +find = { where = ["easierdocker"] } + + +[project] +name = "easier-docker" +version = "2.2.4" +description = "Configure your container image information more easily in python, allowing the container in docker to execute the configured program you want to execute." +readme = "README.md" +requires-python = ">=3.8" +license = {text = "Apache License 2.0"} +authors = [ + {name = "EnSong Wei", email = "touer0018@gmail.com"} +] + + +[project.urls] +Homepage = "https://github.com/touero/easier-docker" +"Bug Reports" = "https://github.com/touero/easier-docker/issues" +Source = "https://github.com/touero/easier-docker" + + +keywords = "easy, docker, docker sdk, python docker" + + +classifiers = "License :: OSI Approved :: Apache Software License, Programming Language :: Python :: 3.8, Programming Language :: Python :: 3.9, Programming Language :: Python :: 3.10, Programming Language :: Python :: 3.11, Programming Language :: Python :: 3.12" + + +dependencies = "docker~=7.1.0, setuptools~=68.2.0, PyYAML~=6.0.1, wheel~=0.42.0, twine~=4.0.2, coverage==7.4.4" + +[project.scripts] +easier-docker = "easierdocker.__main__:main" diff --git a/requirements.txt b/requirements.txt index e4469ad..a70da8f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ PyYAML~=6.0.1 wheel~=0.42.0 twine~=4.0.2 coverage==7.4.4 +build diff --git a/setup.py b/setup.py deleted file mode 100644 index 353f93a..0000000 --- a/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name='easier-docker', - version='2.2.4', - author='EnSong Wei', - author_email='touer0018@gmail.com', - description='Configure your container image information more easily in python, allowing the container in docker ' - 'to execute the configured program you want to execute.', - long_description=open('README.md', encoding='utf-8').read(), - long_description_content_type='text/markdown', - url='https://github.com/touero/easier-docker', - packages=find_packages(), - license='Apache License 2.0', - install_requires=[ - 'docker~=7.1.0', - 'setuptools~=68.2.0', - 'PyYAML~=6.0.1', - 'wheel~=0.42.0', - 'twine~=4.0.2', - 'coverage==7.4.4' - ], - entry_points={ - 'console_scripts': [ - 'easier-docker=easierdocker.__main__:main', - ], - }, - classifiers=[ - 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - ], - keywords='easy, docker, docker sdk, python docker', - project_urls={ - 'Bug Reports': 'https://github.com/touero/easier-docker/issues', - 'Source': 'https://github.com/touero/easier-docker', - }, -) diff --git a/tests/test_easier_docker.py b/tests/test_easier_docker.py index e439cf3..70c2fff 100644 --- a/tests/test_easier_docker.py +++ b/tests/test_easier_docker.py @@ -1,98 +1,102 @@ -import os - import unittest from unittest.mock import patch, MagicMock +from docker.errors import ImageNotFound, APIError, NotFound, DockerException from easierdocker import EasierDocker +from easierdocker.exceptions import DockerConnectionError, NotFoundImageInDockerHub class TestEasierDocker(unittest.TestCase): - @patch('docker.from_env') - def test_init(self, mock_from_env): - parent_dir = os.path.dirname(os.getcwd()) - host_script = os.path.join(parent_dir, 'example') - container_script = '/path/to/container' - container_config = { - 'image': 'python:3.9', - 'name': 'python_test', - 'volumes': { - f'{host_script}': {'bind': container_script, 'mode': 'rw'} - }, - 'detach': True, - 'command': ["sh", "-c", f'cd {container_script} &&' - 'python docker_example.py'], + def setUp(self): + self.container_config = { + "image": "test_image", + "name": "test_container", + "detach": True } + self.network_config = {"name": "test_network"} + self.easier_docker = EasierDocker(self.container_config, self.network_config) - network_config = { - 'name': 'bridge', - 'driver': 'bridge', - } + @patch("docker.from_env") + def test_docker_connection_error(self, mock_from_env): + mock_from_env.side_effect = DockerException("Docker connection failed") + with self.assertRaises(DockerConnectionError): + EasierDocker(container_config={}, network_config={}) - mock_client = MagicMock() - mock_from_env.return_value = mock_client - easier_docker = EasierDocker(container_config=container_config, network_config={}) - - self.assertEqual(easier_docker._container_config, container_config) - self.assertEqual(easier_docker._network_config, {}) - - mock_from_env.assert_called_once() - self.assertEqual(easier_docker._client, mock_client) - - easier_docker = EasierDocker(container_config=container_config, network_config=network_config) - self.assertEqual(easier_docker._container_config, container_config) - self.assertEqual(easier_docker._network_config, network_config) - - @patch('docker.from_env') - def test_properties(self, mock_from_env): - parent_dir = os.path.dirname(os.getcwd()) - host_script = os.path.join(parent_dir, 'example') - container_script = '/path/to/container' - container_config = { - 'image': 'python:3.9', - 'name': 'python_test', - 'volumes': { - f'{host_script}': {'bind': container_script, 'mode': 'rw'} - }, - 'detach': True, - 'command': ["sh", "-c", f'cd {container_script} &&' - 'python docker_example.py'], - } + @patch("docker.from_env") + def test_init_success(self, mock_from_env): + client_mock = MagicMock() + mock_from_env.return_value = client_mock + docker_instance = EasierDocker(self.container_config) + self.assertIsNotNone(docker_instance.client) - network_config = { - 'name': 'bridge', - 'driver': 'bridge', - } - mock_client = MagicMock() - mock_from_env.return_value = mock_client - easier_docker = EasierDocker(container_config=container_config, network_config=network_config) - self.assertEqual(easier_docker.container_config, container_config) - self.assertEqual(easier_docker.network_config, network_config) - self.assertEqual(easier_docker.client, mock_client) - self.assertEqual(easier_docker.image_name, container_config['image']) - self.assertEqual(easier_docker.container_name, container_config['name']) - - @patch('docker.from_env') - def test_get_images(self, mock_from_env): - parent_dir = os.path.dirname(os.getcwd()) - host_script = os.path.join(parent_dir, 'example') - container_script = '/path/to/container' - container_config = { - 'image': 'python:3.9', - 'name': 'python_test', - 'volumes': { - f'{host_script}': {'bind': container_script, 'mode': 'rw'} - }, - 'detach': True, - 'command': ["sh", "-c", f'cd {container_script} &&' - 'python docker_example.py'], - } + @patch("docker.from_env", side_effect=DockerConnectionError("Docker connection failed")) + def test_init_failure(self, mock_from_env): + with self.assertRaises(DockerConnectionError): + EasierDocker(self.container_config) - network_config = { - 'name': 'bridge', - 'driver': 'bridge', - } - mock_client = MagicMock() - mock_from_env.return_value = mock_client - easier_docker = EasierDocker(container_config=container_config, network_config=network_config) - easier_docker._EasierDocker__get_image() - mock_client.images.get.assert_called_once_with(easier_docker.image_name) + @patch("docker.models.images.ImageCollection.get") + def test_get_image_found_locally(self, mock_image_get): + self.easier_docker._EasierDocker__get_image() + mock_image_get.assert_called_once_with("test_image") + + @patch("docker.models.images.ImageCollection.get", side_effect=ImageNotFound("Image not found")) + @patch("docker.api.APIClient.pull", return_value=[ + '{"status": "Pulling", "progress": "50%"}'.encode("utf-8"), + '{"status": "Complete"}'.encode("utf-8") + ]) + def test_get_image_pull_success(self, mock_pull, mock_image_get): + self.easier_docker._EasierDocker__get_image() + mock_pull.assert_called_once_with("test_image", stream=True) + + @patch("docker.models.images.ImageCollection.get", side_effect=ImageNotFound("Image not found")) + @patch("docker.api.APIClient.pull", side_effect=NotFound("Image not in Docker Hub")) + def test_get_image_pull_failure(self, mock_pull, mock_image_get): + with self.assertRaises(NotFoundImageInDockerHub): + self.easier_docker._EasierDocker__get_image() + + @patch("docker.models.containers.ContainerCollection.list", return_value=[]) + @patch("docker.models.containers.ContainerCollection.run") + def test_run_container_success(self, mock_run, mock_list): + container_mock = MagicMock() + container_mock.attrs = {"NetworkSettings": {"IPAddress": "127.0.0.1"}, "Created": "now"} + container_mock.name = "test_container" + container_mock.short_id = "12345" + mock_run.return_value = container_mock + + self.easier_docker._EasierDocker__run_container() + mock_run.assert_called_once_with(**self.container_config) + + @patch("docker.models.containers.ContainerCollection.list", return_value=[]) + @patch("docker.models.containers.ContainerCollection.run", side_effect=APIError("API Error")) + def test_run_container_failure(self, mock_run, mock_list): + with self.assertRaises(APIError): + self.easier_docker._EasierDocker__run_container() + + @patch("docker.models.containers.ContainerCollection.list", return_value=[MagicMock()]) + def test_get_container_found(self, mock_list): + container_mock = mock_list.return_value[0] + container_mock.name = "test_container" + container_mock.attrs = {"NetworkSettings": {"IPAddress": "127.0.0.1"}, "Created": "now"} + container_mock.start = MagicMock() + + container = self.easier_docker._EasierDocker__get_container() + self.assertEqual(container_mock, container) + container_mock.start.assert_called_once() + + @patch("docker.models.containers.ContainerCollection.list", return_value=[]) + def test_get_container_not_found(self, mock_list): + container = self.easier_docker._EasierDocker__get_container() + self.assertIsNone(container) + + @patch("docker.models.networks.NetworkCollection.list", return_value=[]) + @patch("docker.models.networks.NetworkCollection.create") + def test_create_network(self, mock_create, mock_list): + self.easier_docker._EasierDocker__create_network() + mock_create.assert_called_once_with(**self.network_config) + + @patch("docker.client.DockerClient.networks", new_callable=MagicMock) + def test_create_network_exists(self, mock_networks): + mock_network = MagicMock(name="test_network", short_id="short_id_1") + mock_networks.list.return_value = [mock_network] + self.easier_docker._EasierDocker__create_network() + mock_networks.list.assert_called_once()