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
3 changes: 3 additions & 0 deletions .github/workflows/test-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,8 @@ jobs:
- name: Install the project
run: uv sync --locked --all-extras --dev

- name: Check Python formatting with Black
run: uv run --group dev black --check --diff .

- name: Run tests
run: uv run pytest tests
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
repos:
- repo: https://github.com/psf/black
rev: 24.10.0
hooks:
- id: black
language_version: python3.9
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,20 @@ All contributions must abide by our code of conduct. Please see

Here are a few handy tips and common workflows when developing the Tower CLI.

### Getting Started

1. Install development dependencies:
```bash
uv sync --group dev
```

2. Set up pre-commit hooks for code formatting:
```bash
uv run --group dev pre-commit install
```

This will automatically run Black formatter on Python files before each commit.

### Python SDK development

We use `uv` for all development. You can spawn a REPL in context using `uv` very
Expand All @@ -187,6 +201,18 @@ uv sync --locked --all-extras --dev
uv run pytest tests
```

### Code Formatting

We use Black for Python code formatting. The pre-commit hooks will automatically format your code, but you can also run it manually:

```bash
# Format all Python files in the project
uv run --group dev black .

# Check formatting without making changes
uv run --group dev black --check .
```

If you need to get the latest OpenAPI SDK, you can run
`./scripts/generate-python-api-client.sh`.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from faker import Faker


def main():
fake = Faker()
print(fake.name())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
import dlt

print(dlt.version.__version__)
22 changes: 22 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,31 @@ include = ["rust-toolchain.toml"]
[tool.uv.sources]
tower = { workspace = true }

[tool.black]
line-length = 88
target-version = ['py39']
include = '\.pyi?$'
extend-exclude = '''
/(
# directories
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
'''

[dependency-groups]
dev = [
"black==24.10.0",
"openapi-python-client==0.24.3",
"pre-commit==4.0.1",
"pytest==8.3.5",
"pytest-httpx==0.35.0",
"pytest-env>=1.1.3",
Expand Down
113 changes: 88 additions & 25 deletions scripts/semver.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

SEMVER_EXP = re.compile(r"\d+\.\d+(\.\d+)?(-rc\.(\d+))?")


class Version:
def __init__(self, version_str):
version_str = version_str.removeprefix("v")
Expand Down Expand Up @@ -43,21 +44,40 @@ def is_valid(self):

def __eq__(self, other):
if isinstance(other, Version):
return self.major == other.major and self.minor == other.minor and self.patch == other.patch
return (
self.major == other.major
and self.minor == other.minor
and self.patch == other.patch
)
else:
return False

def to_tag_string(self):
if self.prerelease > 0:
return "{major}.{minor}.{patch}-rc.{prerelease}".format(major=self.major, minor=self.minor, patch=self.patch, prerelease=self.prerelease)
return "{major}.{minor}.{patch}-rc.{prerelease}".format(
major=self.major,
minor=self.minor,
patch=self.patch,
prerelease=self.prerelease,
)
else:
return "{major}.{minor}.{patch}".format(major=self.major, minor=self.minor, patch=self.patch)
return "{major}.{minor}.{patch}".format(
major=self.major, minor=self.minor, patch=self.patch
)

def to_python_string(self):
if self.prerelease > 0:
return "{major}.{minor}.{patch}rc{prerelease}".format(major=self.major, minor=self.minor, patch=self.patch, prerelease=self.prerelease)
return "{major}.{minor}.{patch}rc{prerelease}".format(
major=self.major,
minor=self.minor,
patch=self.patch,
prerelease=self.prerelease,
)
else:
return "{major}.{minor}.{patch}".format(major=self.major, minor=self.minor, patch=self.patch)
return "{major}.{minor}.{patch}".format(
major=self.major, minor=self.minor, patch=self.patch
)


def get_all_versions():
# Wait for this to complete.
Expand All @@ -70,13 +90,18 @@ def get_all_versions():
tags = stream.read().split("\n")
return [Version(tag) for tag in tags]


def get_version_set(version):
all_versions = get_all_versions()
return [v for v in all_versions if v.major == version.major and v.minor == version.minor]
return [
v for v in all_versions if v.major == version.major and v.minor == version.minor
]


def get_version_patch(version):
return version.patch


def get_current_version(base):
v = Version(base)
versions = get_version_set(v)
Expand All @@ -101,60 +126,98 @@ def get_current_version(base):
else:
return same_versions[0]


def get_version_base():
path = os.path.join(BASE_PATH, "version.txt")

with open(path) as file:
line = file.readline().rstrip()
return line


def str2bool(value):
if isinstance(value, bool):
return value
if value.lower() in {'true', 'yes', '1'}:
if value.lower() in {"true", "yes", "1"}:
return True
elif value.lower() in {'false', 'no', '0'}:
elif value.lower() in {"false", "no", "0"}:
return False
else:
raise argparse.ArgumentTypeError('Boolean value expected (true/false).')
raise argparse.ArgumentTypeError("Boolean value expected (true/false).")


def replace_line_with_regex(file_path, pattern, replace_text):
"""
Replace lines matching a regex pattern with replace_text in the specified file.

Args:
file_path (str): Path to the file to modify
pattern (re.Pattern): Regex pattern to match lines
replace_text (str): Text to replace the entire line with
"""
with open(file_path, 'r') as file:
with open(file_path, "r") as file:
content = file.read()

# Use regex to replace lines matching the pattern
new_content = pattern.sub(replace_text + '\n', content)
with open(file_path, 'w') as file:
new_content = pattern.sub(replace_text + "\n", content)

with open(file_path, "w") as file:
file.write(new_content)


def update_cargo_file(version):
pattern = re.compile(r'^\s*version\s*=\s*".*"$', re.MULTILINE)
replace_line_with_regex("Cargo.toml", pattern, f'version = "{version.to_tag_string()}"')
replace_line_with_regex(
"Cargo.toml", pattern, f'version = "{version.to_tag_string()}"'
)


def update_pyproject_file(version):
pattern = re.compile(r'^\s*version\s*=\s*".*"$', re.MULTILINE)
replace_line_with_regex("pyproject.toml", pattern, f'version = "{version.to_python_string()}"')
replace_line_with_regex(
"pyproject.toml", pattern, f'version = "{version.to_python_string()}"'
)


if __name__ == "__main__":
parser = argparse.ArgumentParser(
prog='semver',
description='Manages the semantic versioning of the projects',
epilog='This is the epilog'
prog="semver",
description="Manages the semantic versioning of the projects",
epilog="This is the epilog",
)

parser.add_argument("-i", "--patch", type=str2bool, required=False, default=False, help="Increment the patch version")
parser.add_argument("-p", "--prerelease", type=str2bool, required=False, default=False, help="Include the fact that this is a prerelease version")
parser.add_argument("-r", "--release", type=str2bool, required=False, default=False, help="Remove the perelease designation")
parser.add_argument("-w", "--write", type=str2bool, required=False, default=False, help="Update the various tools in this repository")
parser.add_argument(
"-i",
"--patch",
type=str2bool,
required=False,
default=False,
help="Increment the patch version",
)
parser.add_argument(
"-p",
"--prerelease",
type=str2bool,
required=False,
default=False,
help="Include the fact that this is a prerelease version",
)
parser.add_argument(
"-r",
"--release",
type=str2bool,
required=False,
default=False,
help="Remove the perelease designation",
)
parser.add_argument(
"-w",
"--write",
type=str2bool,
required=False,
default=False,
help="Update the various tools in this repository",
)
args = parser.parse_args()

version_base = get_version_base()
Expand Down Expand Up @@ -183,4 +246,4 @@ def update_pyproject_file(version):
os.system("cargo build")
os.system("uv lock")
else:
print(version.to_tag_string(), end='', flush=True)
print(version.to_tag_string(), end="", flush=True)
27 changes: 15 additions & 12 deletions src/tower/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,14 @@ def run_app(
if e.status_code == 404:
raise AppNotFoundError(name)
else:
raise UnknownException(f"Unexpected status code {e.status_code} when running app {name}")
raise UnknownException(
f"Unexpected status code {e.status_code} when running app {name}"
)


def wait_for_run(
run: Run,
timeout: Optional[float] = 86_400.0, # one day
timeout: Optional[float] = 86_400.0, # one day
raise_on_failure: bool = False,
) -> Run:
"""
Expand Down Expand Up @@ -173,12 +176,12 @@ def wait_for_run(
retries += 1

if retries >= DEFAULT_NUM_TIMEOUT_RETRIES:
raise UnknownException("There was a problem with the Tower API.")
raise UnknownException("There was a problem with the Tower API.")


def wait_for_runs(
runs: List[Run],
timeout: Optional[float] = 86_400.0, # one day
timeout: Optional[float] = 86_400.0, # one day
raise_on_failure: bool = False,
) -> tuple[List[Run], List[Run]]:
"""
Expand Down Expand Up @@ -255,7 +258,7 @@ def wait_for_runs(
retries += 1

if retries >= DEFAULT_NUM_TIMEOUT_RETRIES:
raise UnknownException("There was a problem with the Tower API.")
raise UnknownException("There was a problem with the Tower API.")
else:
# Add the item back on the list for retry later on.
awaiting_runs.append(run)
Expand All @@ -273,7 +276,7 @@ def _is_failed_run(run: Run) -> bool:
Returns:
bool: True if the run has failed, False otherwise.
"""
return run.status in ["crashed", "cancelled", "errored"]
return run.status in ["crashed", "cancelled", "errored"]


def _is_successful_run(run: Run) -> bool:
Expand Down Expand Up @@ -302,7 +305,9 @@ def _is_run_awaiting_completion(run: Run) -> bool:
return run.status in ["pending", "scheduled", "running"]


def _env_client(ctx: TowerContext, timeout: Optional[float] = None) -> AuthenticatedClient:
def _env_client(
ctx: TowerContext, timeout: Optional[float] = None
) -> AuthenticatedClient:
tower_url = ctx.tower_url

if not tower_url.endswith("/v1"):
Expand Down Expand Up @@ -338,15 +343,13 @@ def _time_since(start_time: float) -> float:
def _check_run_status(
ctx: TowerContext,
run: Run,
timeout: Optional[float] = 2.0, # one day
timeout: Optional[float] = 2.0, # one day
) -> Run:
client = _env_client(ctx, timeout=timeout)

try:
output: Optional[Union[DescribeRunResponse, ErrorModel]] = describe_run_api.sync(
name=run.app_name,
seq=run.number,
client=client
output: Optional[Union[DescribeRunResponse, ErrorModel]] = (
describe_run_api.sync(name=run.app_name, seq=run.number, client=client)
)

if output is None:
Expand Down
Loading
Loading