Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, '3.10', '3.11', '3.12', '3.13', '3.14']
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
fail-fast: false
steps:
- uses: actions/checkout@v2
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# Yellowbox Changelog
## 0.10.0
### Removed
* dropped support for python 3.8, 3.9
### Changed
* use typing style of Python 3.10+
## 0.9.0
### Changed
* Kafka: bitnami is gone! use `yellowbox-kraft` instead (deprecation warning)
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ poetry run sh scripts/lint.sh
# run testing
poetry run sh scripts/unittest.sh
```
The tests should run and pass for python 3.8, 3.9, 3.10, 3.11, 3.12, 3.14, across Windows, macOS and Linux. You should also add new tests for whatever issue or feature you fixed/added, and add it to `CHANGELOG.md`.
The tests should run and pass for python 3.10, 3.11, 3.12, 3.14, across Windows, macOS and Linux. You should also add new tests for whatever issue or feature you fixed/added, and add it to `CHANGELOG.md`.

# Submit your PR
Submit a PR with changes. Your code should be well documented and efficient. Your code must pass the unit tests as well as receive an approval from at least one code owner.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ readme = "README.md"
repository = "https://github.com/biocatchltd/yellowbox"

[tool.poetry.dependencies]
python = "^3.8"
python = "^3.10"
docker = ">=4.2.0"
yaspin = ">=1.0.0"
requests = "*"
Expand Down
2 changes: 0 additions & 2 deletions tests/test_container_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from __future__ import annotations

import io
import tempfile
from time import sleep
Expand Down
46 changes: 15 additions & 31 deletions yellowbox/containers.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,14 @@
from __future__ import annotations

import io
import os
import shutil
import stat
import tarfile
from collections.abc import Container as AbstractContainer, Generator, Sequence
from contextlib import contextmanager
from functools import lru_cache
from os import PathLike
from tempfile import TemporaryFile
from typing import (
IO,
Any,
Container as AbstractContainer,
Dict,
Generator,
List,
Optional,
Sequence,
TypeVar,
Union,
cast,
overload,
)
from typing import IO, Any, TypeVar, cast, overload

import docker
from docker import DockerClient
Expand Down Expand Up @@ -50,7 +36,7 @@
_CT = TypeVar("_CT", bound=Container)


def get_ports(container: Container) -> Dict[int, int]:
def get_ports(container: Container) -> dict[int, int]:
"""Get the exposed (published) ports of a given container

Useful for when the ports are assigned dynamically.
Expand Down Expand Up @@ -90,7 +76,7 @@ def get_ports(container: Container) -> Dict[int, int]:
return ports


def get_aliases(container: Container, network: Union[str, Network]) -> Sequence[str]:
def get_aliases(container: Container, network: str | Network) -> Sequence[str]:
if not isinstance(network, str):
network = network.name
network_attrs = container.attrs["NetworkSettings"]["Networks"][network]
Expand Down Expand Up @@ -119,9 +105,7 @@ def is_alive(container: Container) -> bool:


@contextmanager
def killing(
container: _CT, *, timeout: float = _DEFAULT_TIMEOUT, signal: str = "SIGKILL"
) -> Generator[_CT, None, None]:
def killing(container: _CT, *, timeout: float = _DEFAULT_TIMEOUT, signal: str = "SIGKILL") -> Generator[_CT]:
"""A context manager that kills a docker container upon completion.

Example:
Expand Down Expand Up @@ -149,8 +133,8 @@ def killing(

@contextmanager
def removing(
container: _CT, *, expected_exit_code: Optional[Union[int, AbstractContainer[int]]] = 0, force=False
) -> Generator[_CT, None, None]:
container: _CT, *, expected_exit_code: int | AbstractContainer[int] | None = 0, force=False
) -> Generator[_CT]:
"""A context manager that removes a docker container upon completion.

Example:
Expand Down Expand Up @@ -238,7 +222,7 @@ def create_and_pull(docker_client: DockerClient, image: str, *args, _kwargs=None
return docker_client.containers.create(local_image_spec, *args, **kwargs)


def create_and_pull_with_defaults(*args, _kwargs: Optional[Dict[str, Any]] = None, **default_kwargs):
def create_and_pull_with_defaults(*args, _kwargs: dict[str, Any] | None = None, **default_kwargs):
if _kwargs:
kwargs = {**default_kwargs, **_kwargs}
else:
Expand All @@ -255,7 +239,7 @@ def is_removed(container: Container):
return False


def download_file(container: Container, path: Union[str, PathLike]) -> IO[bytes]:
def download_file(container: Container, path: str | PathLike) -> IO[bytes]:
"""Download a file from the given container

Args:
Expand Down Expand Up @@ -293,18 +277,18 @@ def download_file(container: Container, path: Union[str, PathLike]) -> IO[bytes]


@overload
def upload_file(container: Container, path: Union[str, PathLike[str]], data: bytes) -> None: ...
def upload_file(container: Container, path: str | PathLike[str], data: bytes) -> None: ...


@overload
def upload_file(container: Container, path: Union[str, PathLike[str]], *, fileobj: IO[bytes]) -> None: ...
def upload_file(container: Container, path: str | PathLike[str], *, fileobj: IO[bytes]) -> None: ...


def upload_file(
container: Container,
path: Union[str, PathLike[str]],
data: Optional[bytes] = None,
fileobj: Optional[IO[bytes]] = None,
path: str | PathLike[str],
data: bytes | None = None,
fileobj: IO[bytes] | None = None,
) -> None:
"""Upload a file to the given container

Expand Down Expand Up @@ -368,7 +352,7 @@ class SafeContainerCreator:

def __init__(self, client: DockerClient):
self.client = client
self.created: List[Container] = []
self.created: list[Container] = []

def create_and_pull(self, image, command=None, **kwargs):
try:
Expand Down
12 changes: 6 additions & 6 deletions yellowbox/extras/aerospike.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from contextlib import contextmanager
from typing import Any, Dict, Optional
from typing import Any

import aerospike
from aerospike import exception as aerospike_exception
Expand All @@ -24,7 +24,7 @@ def __init__(
docker_client: DockerClient,
image="aerospike:ce-6.2.0.3",
*,
container_create_kwargs: Optional[Dict[str, Any]] = None,
container_create_kwargs: dict[str, Any] | None = None,
**kwargs,
):
container = create_and_pull_with_defaults(
Expand All @@ -38,7 +38,7 @@ def __init__(
def client_port(self):
return get_ports(self.container)[AEROSPIKE_DEFAULT_PORT]

def _client(self, config: Optional[Dict[str, Any]] = None) -> aerospike.Client:
def _client(self, config: dict[str, Any] | None = None) -> aerospike.Client:
config = config or {}
config = {
**config,
Expand All @@ -47,14 +47,14 @@ def _client(self, config: Optional[Dict[str, Any]] = None) -> aerospike.Client:
return aerospike.client(config)

@contextmanager
def client(self, config: Optional[Dict[str, Any]] = None):
def client(self, config: dict[str, Any] | None = None):
ret = self._client(config)
try:
yield ret
finally:
ret.close()

def start(self, retry_spec: Optional[RetrySpec] = None, **kwargs):
def start(self, retry_spec: RetrySpec | None = None, **kwargs):
super().start()
retry_spec = retry_spec or RetrySpec(attempts=15)
# for some reason it takes time before the container is ready to get a connection
Expand All @@ -63,7 +63,7 @@ def start(self, retry_spec: Optional[RetrySpec] = None, **kwargs):
self.started = True
return self

async def astart(self, retry_spec: Optional[RetrySpec] = None, **kwargs) -> None:
async def astart(self, retry_spec: RetrySpec | None = None, **kwargs) -> None:
super().start()
retry_spec = retry_spec or RetrySpec(attempts=15)
client = await retry_spec.aretry(self._client, AerospikeError)
Expand Down
15 changes: 7 additions & 8 deletions yellowbox/extras/azure_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
Azure Blob Storage module, for creating container, uploading files to it and downloading files.
"""

from __future__ import annotations

from typing import Any, Dict, List, Optional, Sequence, Union
from collections.abc import Sequence
from typing import Any

from docker import DockerClient
from docker.models.networks import Network
Expand Down Expand Up @@ -41,7 +40,7 @@ def __init__(
docker_client: DockerClient,
image: str = "mcr.microsoft.com/azure-storage/azurite:latest",
*,
container_create_kwargs: Optional[Dict[str, Any]] = None,
container_create_kwargs: dict[str, Any] | None = None,
**kwargs,
):
container = create_and_pull_with_defaults(
Expand All @@ -53,7 +52,7 @@ def __init__(
)
super().__init__(container, **kwargs)

def stop(self, signal: Union[str, int] = "SIGKILL"):
def stop(self, signal: str | int = "SIGKILL"):
"""
We override to change the signal.
"""
Expand Down Expand Up @@ -116,23 +115,23 @@ def _check_ready(self):
if b"Azurite Blob service successfully listens on" not in self.container.logs():
raise _ResourceNotReadyError

def start(self, retry_spec: Optional[RetrySpec] = None):
def start(self, retry_spec: RetrySpec | None = None):
super().start()

retry_spec = retry_spec or RetrySpec(attempts=10)

retry_spec.retry(self._check_ready, _ResourceNotReadyError)
return self

async def astart(self, retry_spec: Optional[RetrySpec] = None):
async def astart(self, retry_spec: RetrySpec | None = None):
super().start()

retry_spec = retry_spec or RetrySpec(attempts=10)

await retry_spec.aretry(self._check_ready, _ResourceNotReadyError)
return self

def connect(self, network: Network, aliases: Optional[List[str]] = None, **kwargs) -> Sequence[str]:
def connect(self, network: Network, aliases: list[str] | None = None, **kwargs) -> Sequence[str]:
# Make sure the id is in the aliases list. Needed for the container
# connection string.
if aliases is not None:
Expand Down
19 changes: 10 additions & 9 deletions yellowbox/extras/fake_gcs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from collections.abc import Iterable
from contextlib import contextmanager
from os import environ, getenv
from typing import Any, Dict, Iterable, Optional
from typing import Any
from warnings import warn

import requests
Expand All @@ -22,7 +23,7 @@ def __init__(
scheme: str = "https",
command: str = "",
*,
container_create_kwargs: Optional[Dict[str, Any]] = None,
container_create_kwargs: dict[str, Any] | None = None,
**kwargs,
):
# note that fake-gcs-server 1.43.0 has a bug https://github.com/fsouza/fake-gcs-server/issues/1034
Expand All @@ -36,31 +37,31 @@ def __init__(
def client_port(self):
return get_ports(self.container)[FAKE_GCS_DEFAULT_PORT]

def local_url(self, scheme: Optional[str] = ...): # type: ignore[assignment]
def local_url(self, scheme: str | None = ...): # type: ignore[assignment]
ret = f"{DOCKER_EXPOSE_HOST}:{self.client_port()}"
if scheme is ...:
scheme = self.scheme
if scheme:
ret = f"{self.scheme}://{ret}"
return ret

def container_url(self, hostname: str, scheme: Optional[str] = ...): # type: ignore[assignment]
def container_url(self, hostname: str, scheme: str | None = ...): # type: ignore[assignment]
ret = f"{hostname}:{FAKE_GCS_DEFAULT_PORT}"
if scheme is ...:
scheme = self.scheme
if scheme:
ret = f"{self.scheme}://{ret}"
return ret

def host_url(self, scheme: Optional[str] = ...): # type: ignore[assignment]
def host_url(self, scheme: str | None = ...): # type: ignore[assignment]
ret = f"{docker_host_name}:{self.client_port()}"
if scheme is ...:
scheme = self.scheme
if scheme:
ret = f"{self.scheme}://{ret}"
return ret

def start(self, retry_spec: Optional[RetrySpec] = None):
def start(self, retry_spec: RetrySpec | None = None):
super().start()
url = self.local_url() + "/storage/v1/b"
retry_spec = retry_spec or RetrySpec(attempts=15)
Expand All @@ -69,7 +70,7 @@ def start(self, retry_spec: Optional[RetrySpec] = None):
)
return self

async def astart(self, retry_spec: Optional[RetrySpec] = None):
async def astart(self, retry_spec: RetrySpec | None = None):
super().start()
url = self.local_url() + "/storage/v1/b"
retry_spec = retry_spec or RetrySpec(attempts=15)
Expand Down Expand Up @@ -123,15 +124,15 @@ def patch_gcloud_aio(self):
gcloud_module.STORAGE_EMULATOR_HOST,
) = previous_state

def create_bucket(self, bucket_name: str) -> Dict[str, Any]:
def create_bucket(self, bucket_name: str) -> dict[str, Any]:
url = self.local_url()

resp = requests.post(url + "/storage/v1/b", json={"name": bucket_name}, verify=False)
resp.raise_for_status()

return resp.json()

def clear_bucket(self, bucket_name: str, prefix: Optional[str] = None) -> Iterable[str]:
def clear_bucket(self, bucket_name: str, prefix: str | None = None) -> Iterable[str]:
url = self.local_url()
params = {}
if prefix:
Expand Down
Loading