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/binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ jobs:
-v "${PWD}:/w" -w /w \
"${IMAGE}" sh -c '
if [ "${ASSERT_NO_PYTHON}" = 1 ]; then
for candidate in python python3 python3.10 python3.11 python3.12 python3.13 python3.14 pypy3; do
for candidate in python python3 python3.11 python3.12 python3.13 python3.14 pypy3; do
if command -v "${candidate}" >/dev/null 2>&1; then
echo "${candidate} present in clean-room image"
exit 1
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
strategy:
fail-fast: ${{ github.event_name == 'pull_request' }}
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
python-version: ['3.11', '3.12', '3.13', '3.14']

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pre-commit install

All dependencies live in `pyproject.toml` (production under `[project.dependencies]`, tooling under the `dev`, `binary`, and `release` dependency groups); exact versions are pinned in `uv.lock`, which is what CI uses. Regenerate the lock with `uv lock` after editing deps.

Python `>=3.10` is required (CI tests 3.10–3.14).
Python `>=3.11` is required (CI tests 3.11–3.14).

## Common commands

Expand Down
6 changes: 3 additions & 3 deletions cloudsmith_cli/cli/commands/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import os
import shlex
import time
from datetime import datetime, timezone
from datetime import UTC, datetime

import click

Expand Down Expand Up @@ -681,7 +681,7 @@ def display_status(current):
fg="cyan",
)

start = datetime.now(tz=timezone.utc)
start = datetime.now(tz=UTC)
context_msg = "Failed to synchronise file!"
with handle_api_exceptions(
ctx, opts=opts, context_msg=context_msg, reraise_on_error=skip_errors
Expand Down Expand Up @@ -738,7 +738,7 @@ def display_status(current):
if left > 0:
pb.update(left)

end = datetime.now(tz=timezone.utc)
end = datetime.now(tz=UTC)
seconds = (end - start).total_seconds()

click.echo(err=use_stderr)
Expand Down
6 changes: 3 additions & 3 deletions cloudsmith_cli/cli/tests/commands/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import json
import webbrowser
from datetime import datetime, timezone
from datetime import UTC, datetime
from unittest.mock import ANY, MagicMock, patch

import jwt
Expand Down Expand Up @@ -269,7 +269,7 @@ def test_auth_command_passes_owner_to_webserver(
assert call_kwargs.get("owner") == "testorg"

def test_usable_session_avoids_workspace_and_browser(self, runner):
expires_at = datetime(2030, 1, 2, 3, 4, 5, tzinfo=timezone.utc)
expires_at = datetime(2030, 1, 2, 3, 4, 5, tzinfo=UTC)
access_token = jwt.encode(
{"exp": expires_at},
"not-used-for-verification",
Expand Down Expand Up @@ -301,7 +301,7 @@ def test_usable_session_avoids_workspace_and_browser(self, runner):

@pytest.mark.parametrize("output_format", ["json", "pretty_json"])
def test_json_renewal_report_is_machine_readable(self, runner, output_format):
expires_at = datetime(2030, 1, 2, 3, 4, 5, tzinfo=timezone.utc)
expires_at = datetime(2030, 1, 2, 3, 4, 5, tzinfo=UTC)
renewal = SsoRenewalResult(
status=SsoRenewalStatus.CURRENT,
access_token=jwt.encode(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
must rank domains identically to the run that populated it.
"""

from datetime import datetime, timezone
from datetime import UTC, datetime
from unittest.mock import patch

from ....credential_helpers.backends import BackendKind
Expand Down Expand Up @@ -100,7 +100,7 @@ def test_precedence_fields_survive_the_api_and_the_cache(tmp_path):
"validated": True,
"primary": False,
"repository": {"name": "Prod", "slug": "prod"},
"created_at": datetime(2025, 3, 4, 9, 22, 30, tzinfo=timezone.utc),
"created_at": datetime(2025, 3, 4, 9, 22, 30, tzinfo=UTC),
},
"acme",
)
Expand Down
2 changes: 0 additions & 2 deletions cloudsmith_cli/cli/tests/commands/test_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
from contextlib import ExitStack
from unittest.mock import patch

import pytest

from ....cli.commands.update import update
from ....core import installation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
import click.testing
import pytest

from ...credential_helpers.terraform import installer as installer_mod, terraformrc
from ...credential_helpers.terraform import installer as installer_mod
from ...credential_helpers.terraform import terraformrc
from ...credential_helpers.terraform.installer import (
TerraformHelperExeNotFound,
TerraformInstaller,
Expand Down
4 changes: 2 additions & 2 deletions cloudsmith_cli/cli/tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime
from uuid import uuid4


Expand All @@ -9,4 +9,4 @@ def random_str():

def random_bool():
"""Return a random bool."""
return datetime.now(tz=timezone.utc).microsecond % 2 == 0
return datetime.now(tz=UTC).microsecond % 2 == 0
4 changes: 2 additions & 2 deletions cloudsmith_cli/cli/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import base64
import re
from datetime import datetime, timezone
from datetime import UTC, datetime
from urllib.parse import urlsplit

import click
Expand Down Expand Up @@ -299,7 +299,7 @@ def validate_optional_timestamp(ctx, param, value):
try:
return (
datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ")
.replace(tzinfo=timezone.utc)
.replace(tzinfo=UTC)
.replace(hour=0, minute=0, second=0)
)
except ValueError:
Expand Down
10 changes: 4 additions & 6 deletions cloudsmith_cli/core/keyring.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import getpass
import os
import sys
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta

from .utils import controlling_terminal_mode, is_interactive

Expand Down Expand Up @@ -225,7 +225,7 @@ def update_refresh_attempted_at(api_host, refresh_time=None, profile=None):
from keyring.errors import KeyringError

if refresh_time is None:
refresh_time = datetime.now(tz=timezone.utc)
refresh_time = datetime.now(tz=UTC)

refresh_attempted_at_value = refresh_time.isoformat()

Expand Down Expand Up @@ -260,7 +260,7 @@ def should_refresh_access_token(api_host, access_token=None, profile=None):

expires_at = get_access_token_expiry(access_token)
if expires_at is not None:
now = datetime.now(tz=timezone.utc)
now = datetime.now(tz=UTC)
if expires_at > now + timedelta(minutes=30):
return False
if expires_at <= now:
Expand All @@ -273,9 +273,7 @@ def should_refresh_access_token(api_host, access_token=None, profile=None):
token_refreshed_at = get_refresh_attempted_at(api_host, profile=profile)

if token_refreshed_at:
return token_refreshed_at < (
datetime.now(tz=timezone.utc) - timedelta(minutes=30)
)
return token_refreshed_at < (datetime.now(tz=UTC) - timedelta(minutes=30))

return True

Expand Down
2 changes: 1 addition & 1 deletion cloudsmith_cli/core/ratelimits.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def from_dict(cls, data):
info.remaining = int(data["remaining"])
if "reset" in data:
info.reset = datetime.datetime.fromtimestamp(
int(data["reset"]), tz=datetime.timezone.utc
int(data["reset"]), tz=datetime.UTC
)
if "throttled" in data:
info.throttled = bool(data["throttled"])
Expand Down
6 changes: 3 additions & 3 deletions cloudsmith_cli/core/sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import json
import logging
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from enum import Enum

import requests
Expand Down Expand Up @@ -57,7 +57,7 @@ def get_access_token_expiry(access_token):
payload = json.loads(base64.urlsafe_b64decode(encoded_payload + padding))
expires_at = payload.get("exp")
if expires_at is not None:
return datetime.fromtimestamp(float(expires_at), tz=timezone.utc)
return datetime.fromtimestamp(float(expires_at), tz=UTC)
except (
binascii.Error,
IndexError,
Expand Down Expand Up @@ -85,7 +85,7 @@ def access_token_is_valid(access_token, now=None):
if expires_at is None:
return True

return expires_at + TOKEN_EXPIRY_LEEWAY > (now or datetime.now(tz=timezone.utc))
return expires_at + TOKEN_EXPIRY_LEEWAY > (now or datetime.now(tz=UTC))


def _load_sso_tokens(api_host, profile):
Expand Down
20 changes: 10 additions & 10 deletions cloudsmith_cli/core/tests/test_keyring.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import importlib
import os
from contextlib import contextmanager
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from unittest.mock import ANY, Mock, call, patch

import jwt
Expand All @@ -13,7 +13,6 @@
from keyrings.cryptfile.cryptfile import CryptFileKeyring

from .. import keyring as core_keyring
from ..utils import TTYMode
from ..keyring import (
delete_oidc_token,
delete_sso_tokens,
Expand All @@ -30,6 +29,7 @@
store_sso_tokens,
update_refresh_attempted_at,
)
from ..utils import TTYMode


@pytest.fixture
Expand Down Expand Up @@ -104,7 +104,7 @@ def test_get_access_token_when_error_raised(self, mock_get_user, mock_get_passwo

@freeze_time("2024-06-01 10:00:00")
def test_update_refresh_attempted_at(self, mock_get_user, mock_set_password):
attempted_at = datetime.now(tz=timezone.utc).isoformat()
attempted_at = datetime.now(tz=UTC).isoformat()

update_refresh_attempted_at(self.api_host)

Expand All @@ -116,11 +116,11 @@ def test_update_refresh_attempted_at(self, mock_get_user, mock_set_password):

def test_get_refresh_attempted_at(self, mock_get_user, mock_get_password):
mock_get_password.return_value = datetime(
2024, 6, 1, 10, 0, tzinfo=timezone.utc
2024, 6, 1, 10, 0, tzinfo=UTC
).isoformat()

assert get_refresh_attempted_at(self.api_host) == datetime(
2024, 6, 1, hour=10, minute=0, tzinfo=timezone.utc
2024, 6, 1, hour=10, minute=0, tzinfo=UTC
)
mock_get_password.assert_called_once_with(
"cloudsmith_cli-access_token_refresh_attempted_at-https://example.com",
Expand Down Expand Up @@ -155,7 +155,7 @@ def test_get_refresh_attempted_at_when_invalid_datetime_returned(
def test_should_refresh_access_token_with_new_token(
self, mock_get_user, mock_get_password
):
mock_get_password.return_value = datetime.now(tz=timezone.utc).isoformat()
mock_get_password.return_value = datetime.now(tz=UTC).isoformat()

assert not should_refresh_access_token(self.api_host)
mock_get_password.assert_called_once_with(
Expand All @@ -168,7 +168,7 @@ def test_should_refresh_access_token_with_token_about_to_expire(
self, mock_get_user, mock_get_password
):
mock_get_password.return_value = (
datetime.now(tz=timezone.utc) - timedelta(minutes=30)
datetime.now(tz=UTC) - timedelta(minutes=30)
).isoformat()

assert not should_refresh_access_token(self.api_host)
Expand All @@ -182,7 +182,7 @@ def test_should_refresh_access_token_with_expired_token(
self, mock_get_user, mock_get_password
):
mock_get_password.return_value = (
datetime.now(tz=timezone.utc) - timedelta(minutes=31)
datetime.now(tz=UTC) - timedelta(minutes=31)
).isoformat()

assert should_refresh_access_token(self.api_host)
Expand All @@ -205,7 +205,7 @@ def test_jwt_refresh_uses_expiry_and_ignores_throttle_after_expiration(
):
mock_get_password.return_value = attempted_at
access_token = jwt.encode(
{"exp": datetime.fromisoformat(expires_at).replace(tzinfo=timezone.utc)},
{"exp": datetime.fromisoformat(expires_at).replace(tzinfo=UTC)},
"not-used-for-verification",
algorithm="HS256",
)
Expand Down Expand Up @@ -436,7 +436,7 @@ def test_should_refresh_access_token_with_profile(
self, mock_get_user, mock_get_password
):
mock_get_password.return_value = (
datetime.now(tz=timezone.utc) - timedelta(minutes=31)
datetime.now(tz=UTC) - timedelta(minutes=31)
).isoformat()

assert should_refresh_access_token(self.api_host, profile="staging")
Expand Down
4 changes: 2 additions & 2 deletions cloudsmith_cli/core/tests/test_sso.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Tests for shared SSO session renewal."""

from datetime import datetime, timezone
from datetime import UTC, datetime
from unittest.mock import MagicMock, call, patch

import jwt
Expand Down Expand Up @@ -42,7 +42,7 @@ def test_successful_renewal_stores_rotated_tokens():
@freeze_time("2024-06-01 10:00:00")
def test_transient_failure_reuses_usable_access_token():
access_token = jwt.encode(
{"exp": datetime(2024, 6, 1, 9, 59, 31, tzinfo=timezone.utc)},
{"exp": datetime(2024, 6, 1, 9, 59, 31, tzinfo=UTC)},
"not-used-for-verification",
algorithm="HS256",
)
Expand Down
3 changes: 1 addition & 2 deletions cloudsmith_cli/credential_helpers/pnpm/rc.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Copyright 2026 Cloudsmith Ltd
from pathlib import Path

from typing_extensions import Self
from typing import Self


class AuthKeyConflictError(KeyError):
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ license-files = ["LICENSE"]
authors = [
{ name = "Cloudsmith Ltd", email = "support@cloudsmith.io" },
]
requires-python = ">=3.10.0"
requires-python = ">=3.11.0"
keywords = ["cloudsmith", "cli", "devops"]
classifiers = [
# As from http://pypi.python.org/pypi?%3Aaction=list_classifiers
Expand All @@ -28,7 +28,6 @@ classifiers = [
"Intended Audience :: System Administrators",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
Expand Down
Loading
Loading