diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 328209484..f0b7492ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,11 +30,11 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] os: [ubuntu-latest, windows-latest, macos-latest] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run spell check on Ubuntu if: matrix.os == 'ubuntu-latest' uses: codespell-project/actions-codespell@master @@ -59,7 +59,7 @@ jobs: echo "/Users/runner/.local/bin" >> $GITHUB_PATH - name: Run unit tests run: | - python setup.py install + pip install -e . pytest - name: Run functional tests on Ubuntu if: matrix.os == 'ubuntu-latest' diff --git a/minio/api.py b/minio/api.py index 5d1f5b4c9..d70eadcb3 100644 --- a/minio/api.py +++ b/minio/api.py @@ -165,7 +165,8 @@ def __init__( ) def __del__(self): - self._http.clear() + if hasattr(self, "_http"): # Only required for unit test run + self._http.clear() def _handle_redirect_response( self, method, bucket_name, response, retry=False, diff --git a/minio/error.py b/minio/error.py index f73691324..530bf8051 100644 --- a/minio/error.py +++ b/minio/error.py @@ -45,8 +45,8 @@ def __init__(self, code, content_type, body): self._content_type = content_type self._body = body super().__init__( - f"non-XML response from server; " - f"Response code: {code}, Content-Type: {content_type}, Body: {body}" + f"non-XML response from server; Response code: {code}, " + f"Content-Type: {content_type}, Body: {body}" ) def __reduce__(self): diff --git a/minio/helpers.py b/minio/helpers.py index a4617dced..f68bc987a 100644 --- a/minio/helpers.py +++ b/minio/helpers.py @@ -123,11 +123,11 @@ def _validate_sizes(object_size, part_size): if part_size > 0: if part_size < MIN_PART_SIZE: raise ValueError( - f"part size {part_size} is not supported; minimum allowed 5MiB", + f"part size {part_size} is not supported; minimum allowed 5MiB" ) if part_size > MAX_PART_SIZE: raise ValueError( - f"part size {part_size} is not supported; maximum allowed 5GiB", + f"part size {part_size} is not supported; maximum allowed 5GiB" ) if object_size >= 0: diff --git a/minio/select.py b/minio/select.py index 2e41e8a43..5320a4e8b 100644 --- a/minio/select.py +++ b/minio/select.py @@ -391,7 +391,8 @@ def _read(self): if headers.get(":message-type") == "error": raise MinioException( - f"{headers.get(':error-code')}: {headers.get(':error-message')}" + f"{headers.get(':error-code')}: " + f"{headers.get(':error-message')}" ) if headers.get(":event-type") == "End": diff --git a/minio/sse.py b/minio/sse.py index eef5019a7..04f01b277 100644 --- a/minio/sse.py +++ b/minio/sse.py @@ -27,6 +27,7 @@ import base64 import json from abc import ABCMeta, abstractmethod +from typing import Dict class Sse: @@ -34,14 +35,14 @@ class Sse: __metaclass__ = ABCMeta @abstractmethod - def headers(self): + def headers(self) -> Dict[str, str]: """Return headers.""" - def tls_required(self): # pylint: disable=no-self-use + def tls_required(self) -> bool: # pylint: disable=no-self-use """Return TLS required to use this server-side encryption.""" return True - def copy_headers(self): # pylint: disable=no-self-use + def copy_headers(self) -> Dict[str, str]: # pylint: disable=no-self-use """Return copy headers.""" return {} @@ -49,7 +50,7 @@ def copy_headers(self): # pylint: disable=no-self-use class SseCustomerKey(Sse): """ Server-side encryption - customer key type.""" - def __init__(self, key): + def __init__(self, key: bytes): if len(key) != 32: raise ValueError( "SSE-C keys need to be 256 bit base64 encoded", @@ -71,17 +72,17 @@ def __init__(self, key): md5key, } - def headers(self): + def headers(self) -> Dict[str, str]: return self._headers.copy() - def copy_headers(self): + def copy_headers(self) -> Dict[str, str]: return self._copy_headers.copy() class SseKMS(Sse): """Server-side encryption - KMS type.""" - def __init__(self, key, context): + def __init__(self, key: str, context: Dict): self._headers = { "X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": key, "X-Amz-Server-Side-Encryption": "aws:kms" @@ -92,17 +93,17 @@ def __init__(self, key, context): base64.b64encode(data).decode() ) - def headers(self): + def headers(self) -> Dict[str, str]: return self._headers.copy() class SseS3(Sse): """Server-side encryption - S3 type.""" - def headers(self): + def headers(self) -> Dict[str, str]: return { "X-Amz-Server-Side-Encryption": "AES256" } - def tls_required(self): + def tls_required(self) -> bool: return False diff --git a/minio/time.py b/minio/time.py index a3c4a3dfc..00db6ad8d 100644 --- a/minio/time.py +++ b/minio/time.py @@ -21,6 +21,12 @@ import time as ctime from datetime import datetime, timezone +try: + from datetime import UTC + _UTC_IMPORTED = True +except ImportError: + _UTC_IMPORTED = False + _WEEK_DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] _MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] @@ -102,6 +108,8 @@ def to_amz_date(value): def utcnow(): """Timezone-aware wrapper to datetime.utcnow().""" + if _UTC_IMPORTED: + return datetime.now(UTC).replace(tzinfo=timezone.utc) return datetime.utcnow().replace(tzinfo=timezone.utc) diff --git a/tests/functional/tests.py b/tests/functional/tests.py index fcbd89bb2..7def5ba85 100644 --- a/tests/functional/tests.py +++ b/tests/functional/tests.py @@ -140,7 +140,9 @@ def _call_test(func, *args, **kwargs): if log_entry.get("method"): # pylint: disable=deprecated-method args_string = ', '.join(getfullargspec(log_entry["method"]).args[1:]) - log_entry["function"] = f"{log_entry['method'].__name__}({args_string})" + log_entry["function"] = ( + f"{log_entry['method'].__name__}({args_string})" + ) log_entry["args"] = { k: v for k, v in log_entry.get("args", {}).items() if v } diff --git a/tests/unit/crypto_test.py b/tests/unit/crypto_test.py index 367399a46..4faf83bfa 100644 --- a/tests/unit/crypto_test.py +++ b/tests/unit/crypto_test.py @@ -25,7 +25,10 @@ def test_correct(self): plaintext = "Hello MinIO!" encrypted = encrypt(plaintext.encode(), secret) decrypted = decrypt(encrypted, secret).decode() - self.assertEquals(plaintext, decrypted) + if hasattr(self, "assertEquals"): + self.assertEquals(plaintext, decrypted) + else: + self.assertEqual(plaintext, decrypted) def test_wrong(self): secret = "topsecret" diff --git a/tests/unit/minio_mocks.py b/tests/unit/minio_mocks.py index 3c0a85367..19aa6d51e 100644 --- a/tests/unit/minio_mocks.py +++ b/tests/unit/minio_mocks.py @@ -69,6 +69,9 @@ class MockConnection(object): def __init__(self): self.requests = [] + def clear(self): + pass + def mock_add_request(self, request): self.requests.append(request) diff --git a/tests/unit/retention_test.py b/tests/unit/retention_test.py index 4f9f6bb7e..58487e615 100644 --- a/tests/unit/retention_test.py +++ b/tests/unit/retention_test.py @@ -20,11 +20,12 @@ from minio import xml from minio.commonconfig import COMPLIANCE, GOVERNANCE from minio.retention import Retention +from minio.time import utcnow class RetentionTest(TestCase): def test_config(self): - config = Retention(GOVERNANCE, datetime.utcnow() + timedelta(days=10)) + config = Retention(GOVERNANCE, utcnow() + timedelta(days=10)) xml.marshal(config) config = xml.unmarshal(