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
22 changes: 11 additions & 11 deletions .github/workflows/build-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
target:
- x86_64-unknown-linux-gnu
platform:
- target: x86_64-unknown-linux-gnu
python-version: ["3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v6
Expand All @@ -212,7 +212,7 @@ jobs:
- name: Build wheel
uses: PyO3/maturin-action@v1.50.1
with:
target: ${{ matrix.target }}
target: ${{ matrix.platform.target }}
manylinux: auto
args: --release --locked --out dist -i python${{ matrix.python-version }}
before-script-linux: |
Expand All @@ -225,7 +225,7 @@ jobs:
sudo apt-get install -y libssl-dev openssl pkg-config
fi
- name: Test wheel
if: ${{ startsWith(matrix.target, 'x86_64') }}
if: ${{ startsWith(matrix.platform.target, 'x86_64') }}
run: |
PYTAG="cp$(python -c "import sys; print(f'{sys.version_info.major}{sys.version_info.minor}')")"
pip install dist/*-${PYTAG}-*.whl --force-reinstall
Expand All @@ -234,7 +234,7 @@ jobs:
- name: Upload wheel
uses: actions/upload-artifact@v6
with:
name: wheels-${{ matrix.target }}
name: wheels-${{ matrix.platform.target }}-py${{ matrix.python-version }}
path: dist

linux-cross:
Expand Down Expand Up @@ -302,8 +302,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
target:
- x86_64-unknown-linux-musl
platform:
- target: x86_64-unknown-linux-musl
python-version: ["3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v6
Expand All @@ -317,11 +317,11 @@ jobs:
- name: Build wheel
uses: PyO3/maturin-action@v1.50.1
with:
target: ${{ matrix.target }}
target: ${{ matrix.platform.target }}
manylinux: musllinux_1_2
args: --release --locked --out dist -i python${{ matrix.python-version }}
- name: Test wheel
if: matrix.target == 'x86_64-unknown-linux-musl'
if: matrix.platform.target == 'x86_64-unknown-linux-musl'
uses: addnab/docker-run-action@v3
with:
image: python:${{ matrix.python-version}}-alpine
Expand All @@ -334,7 +334,7 @@ jobs:
- name: Upload wheel
uses: actions/upload-artifact@v6
with:
name: wheels-${{ matrix.target }}
name: wheels-${{ matrix.platform.target }}-py${{ matrix.python-version }}
path: dist

musllinux-cross:
Expand Down Expand Up @@ -376,5 +376,5 @@ jobs:
- name: Upload wheel
uses: actions/upload-artifact@v6
with:
name: wheels-${{ matrix.platform.target }}
name: wheels-${{ matrix.platform.target }}-py${{ matrix.python-version }}
path: dist
1 change: 0 additions & 1 deletion .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 24
registry-url: 'https://registry.npmjs.org'
- name: Upgrade npm to latest patch
run: npm install -g npm@latest
- name: Publish to npm
Expand Down
22 changes: 11 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ resolver = "2"

[workspace.package]
edition = "2021"
version = "0.3.68"
version = "0.3.69"
description = "Tower is the best way to host Python data apps in production"
rust-version = "1.81"
authors = ["Brad Heller <brad@tower.dev>", "Ben Lovell <ben@tower.dev>"]
Expand Down
1 change: 1 addition & 0 deletions crates/tower-package/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
license = { workspace = true }
repository = { workspace = true }

[lib]
crate-type = ["cdylib", "rlib"]
Expand Down
14 changes: 14 additions & 0 deletions crates/tower-uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,16 @@ impl Uv {
}
}

// Without an explicit cache dir, uv's default cache may be on a different
// filesystem than the target venv, so hardlinking fails and uv warns before
// falling back to a copy. Force copy mode to skip the failed attempt and the
// warning.
fn apply_link_mode(&self, cmd: &mut Command) {
if self.cache_dir.is_none() {
cmd.env("UV_LINK_MODE", "copy");
}
}

pub async fn venv(
&self,
cwd: &PathBuf,
Expand Down Expand Up @@ -361,6 +371,7 @@ impl Uv {
if let Some(dir) = &self.cache_dir {
cmd.arg("--cache-dir").arg(dir);
}
self.apply_link_mode(&mut cmd);

let child = cmd.spawn()?;

Expand Down Expand Up @@ -406,6 +417,7 @@ impl Uv {
.arg("never")
.arg("pip")
.arg("install");
self.apply_link_mode(&mut cmd);
cmd
}

Expand Down Expand Up @@ -494,6 +506,8 @@ impl Uv {

// Need to do this after env_clear intentionally.
cmd.envs(env_vars);
// Also after env_clear so protected mode doesn't wipe it.
self.apply_link_mode(&mut cmd);

if let Some(dir) = &self.cache_dir {
cmd.arg("--cache-dir").arg(dir);
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "tower"
version = "0.3.68"
version = "0.3.69"
description = "Tower CLI and runtime environment for Tower."
authors = [{ name = "Tower Computing Inc.", email = "brad@tower.dev" }]
readme = "README.md"
Expand Down
77 changes: 77 additions & 0 deletions src/tower/_storage.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import hashlib
import logging
import time
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
Expand All @@ -9,6 +10,7 @@

from ._client import _env_client
from ._context import TowerContext
from .tower_api_client.api.default import describe_catalog as describe_catalog_api
from .tower_api_client.api.default import (
describe_default_catalog as describe_default_catalog_api,
)
Expand All @@ -17,6 +19,7 @@
)
from .tower_api_client.models import (
CatalogCredentials,
DescribeCatalogResponse,
ErrorModel,
VendCatalogCredentialsBody,
VendCatalogCredentialsBodyMode,
Expand All @@ -25,9 +28,16 @@
from .tower_api_client.types import UNSET, Unset

CREDENTIAL_REFRESH_WINDOW = timedelta(minutes=5)
# how long to wait for a catalog type describe request to complete
CATALOG_TYPE_DESCRIBE_TIMEOUT_SECONDS = 2.0
# only cache failed catalog type describe requests for this long
# retry only after this period
CATALOG_TYPE_FAILURE_CACHE_TTL_SECONDS = 30.0
DEFAULT_CATALOG_PROVISION_RETRY_DELAYS = (0.25, 0.5, 1.0, 2.0)
DEFAULT_CATALOG_NAME = "default"
DEFAULT_ENVIRONMENT_NAME = "default"
TOWER_CATALOG_TYPE = "tower-catalog"
logger = logging.getLogger("tower.storage")


@dataclass
Expand All @@ -39,7 +49,14 @@ def is_usable(self, now: datetime) -> bool:
return now < expires_at - CREDENTIAL_REFRESH_WINDOW


@dataclass
class _CachedCatalogType:
catalog_type: str | None
retry_at: float | None = None


_credential_cache: dict[tuple[str, str, str, str, str], _CachedCredentials] = {}
_catalog_type_cache: dict[tuple[str, str, str], _CachedCatalogType] = {}


def get_tower_catalog(
Expand Down Expand Up @@ -123,6 +140,65 @@ def _vend_catalog_credentials(
)


def _describe_tower_catalog_type(
ctx: TowerContext, name: str, environment: str
) -> str | None:
if not (ctx.api_key or ctx.jwt):
return None

cache_key = (ctx.tower_url, name, environment)
cached = _catalog_type_cache.get(cache_key)
if cached is not None:
if cached.retry_at is None:
return cached.catalog_type

if time.monotonic() < cached.retry_at:
return None

_catalog_type_cache.pop(cache_key, None)

try:
result = describe_catalog_api.sync(
name=name,
client=_env_client(ctx, timeout=CATALOG_TYPE_DESCRIBE_TIMEOUT_SECONDS),
environment=environment,
)
except Exception:
logger.debug(
"Failed to describe Tower catalog %r in environment %r; "
"falling back to PyIceberg catalog configuration detection.",
name,
environment,
exc_info=True,
)
_catalog_type_cache[cache_key] = _failed_catalog_type_cache_entry()
return None

if isinstance(result, DescribeCatalogResponse):
catalog_type = result.catalog.type_
_catalog_type_cache[cache_key] = _CachedCatalogType(catalog_type=catalog_type)
return catalog_type

if isinstance(result, ErrorModel):
logger.debug(
"Tower catalog describe for %r in environment %r returned %s; "
"falling back to PyIceberg catalog configuration detection.",
name,
environment,
_error_text(result),
)

_catalog_type_cache[cache_key] = _failed_catalog_type_cache_entry()
return None
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def _failed_catalog_type_cache_entry() -> _CachedCatalogType:
return _CachedCatalogType(
catalog_type=None,
retry_at=time.monotonic() + CATALOG_TYPE_FAILURE_CACHE_TTL_SECONDS,
)


def _ensure_legacy_default_catalog(ctx: TowerContext) -> None:
try:
response = describe_default_catalog_api.sync_detailed(client=_env_client(ctx))
Expand Down Expand Up @@ -210,3 +286,4 @@ def _ensure_aware(value: datetime) -> datetime:

def _clear_credential_cache() -> None:
_credential_cache.clear()
_catalog_type_cache.clear()
Loading
Loading