diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..e276bc4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ +## Summary + +- What changed, why and the link to the issue that it's solving. + +## Checklist + +- [ ] CI is green (`lint`, `typecheck`, `test`, `secrets`) +- [ ] `pre-commit run --all-files` passes locally +- [ ] Tests were added or updated when behavior changed +- [ ] Public API / typing changes were reviewed +- [ ] Documentation was updated (`README.md` / `CONTRIBUTING.md`) if needed +- [ ] Breaking changes are clearly documented +- [ ] `CHANGELOG.md` was updated when user-facing behavior changed diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f132706 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,75 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + lint: + name: Lint (ruff) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install uv + run: python -m pip install --upgrade pip uv + - name: Install dependencies + run: uv sync --group dev + - name: Ruff check + run: uv run ruff check . + - name: Ruff format check + run: uv run ruff format --check . + + typecheck: + name: Type check (mypy) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install uv + run: python -m pip install --upgrade pip uv + - name: Install dependencies + run: uv sync --group dev + - name: Run mypy + run: uv run mypy + + test: + name: Test (pytest) + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install uv + run: python -m pip install --upgrade pip uv + - name: Install dependencies + run: uv sync --group dev + - name: Run tests + run: uv run pytest + + secrets: + name: Secrets scan (gitleaks) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install gitleaks (OSS binary) + run: | + GITLEAKS_VERSION="8.28.0" + curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" -o gitleaks.tar.gz + tar -xzf gitleaks.tar.gz + sudo mv gitleaks /usr/local/bin/gitleaks + gitleaks version + - name: Run gitleaks + run: gitleaks detect --source . --verbose --redact diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d22c219..9e276e8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,14 +1,14 @@ name: publish on: - push: - branches: - - main - paths: - - pyproject.toml + workflow_run: + workflows: ["CI"] + types: + - completed jobs: publish: + if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main' }} runs-on: ubuntu-latest environment: name: pypi @@ -19,13 +19,14 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 2 + ref: ${{ github.event.workflow_run.head_sha }} - name: Detect version change id: version_check run: | - BEFORE_SHA="${{ github.event.before }}" - AFTER_SHA="${{ github.sha }}" + BEFORE_SHA="$(git rev-parse HEAD^)" + AFTER_SHA="$(git rev-parse HEAD)" BEFORE_VERSION="$(git show "${BEFORE_SHA}:pyproject.toml" 2>/dev/null | sed -nE 's/^version = "([^"]+)"/\1/p' | head -n1)" AFTER_VERSION="$(sed -nE 's/^version = "([^"]+)"/\1/p' pyproject.toml | head -n1)" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..82568d3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.13.2 + hooks: + - id: ruff + args: ["--fix"] + - id: ruff-format + + - repo: local + hooks: + - id: mypy + name: mypy + entry: uv run mypy + language: system + pass_filenames: false + + - repo: https://github.com/gitleaks/gitleaks + rev: v8.28.0 + hooks: + - id: gitleaks + args: ["--verbose", "--redact"] diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..615b9a7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Professional repository baseline: + - CI workflow with lint, type checks, tests, and secret scanning gates. + - `pre-commit` with `ruff`, `mypy`, and `gitleaks`. + - `Makefile`, `CODEOWNERS`, and PR template. + - Initial `SECURITY.md` and packaging metadata improvements. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e7015f..cfe0bdf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,29 +1,59 @@ -# Contributing to Tinybird +# Contributing to tinybird-sdk-python -Thank you for your interest in contributing to this project! +Thank you for contributing. -## How to Contribute +## Prerequisites -1. **Fork the repository** and create your branch from `main`. -2. **Make your changes** and ensure they follow the project's coding style. -3. **Test your changes** to make sure everything works as expected. -4. **Submit a pull request** with a clear description of your changes. +- Python 3.11+ +- `uv` installed (`pip install uv`) + +## Local Setup + +```bash +uv sync --group dev +uv run pre-commit install +``` + +## Validation Workflow (must pass before PR) + +Run the same checks used in CI: + +```bash +make check +``` + +Or run them individually: + +```bash +make lint +make typecheck +make test +make secrets +``` + +## Pull Request Process + +1. Branch from `main`. +2. Keep changes focused and include tests for behavior changes. +3. Update docs (`README.md`, this file) when usage/workflow changes. +4. Update `CHANGELOG.md` for user-facing changes. +5. Open a PR using the provided template and complete the checklist. + +`CODEOWNERS` is enabled for source, tests, and release/config paths. ## Reporting Issues -If you find a bug or have a feature request, please open an issue on GitHub with: +Open an issue with: -- A clear and descriptive title -- Steps to reproduce the issue (if applicable) -- Expected vs actual behavior -- Any relevant logs or screenshots +- clear title and expected behavior +- reproduction steps +- environment details (Python version, OS) +- logs or stack traces when available -## Code of Conduct +## Security -Please be respectful and constructive in all interactions. We're all here to build something great together. +Do not report vulnerabilities in public issues. See `SECURITY.md`. ## License -This project is licensed under the MIT License. - -By contributing (e.g., submitting a pull request), you agree that your contributions will be licensed under the MIT License, and you represent that you have the authority to make the contribution. +By contributing, you agree that your contributions are licensed under MIT. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9230bec --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +SRC_DIRS=src tests + +.PHONY: help +help: ## Show available commands + @awk -F ':|##' '/^[^\t].+?:.*?##/ { printf "\033[36m%-22s\033[0m %s\n", $$1, $$NF }' $(MAKEFILE_LIST) + +.PHONY: install +install: ## Install dev dependencies with uv + uv sync --group dev + +.PHONY: lint +lint: ## Run ruff lint and format checks + uv run ruff check . + uv run ruff format --check . + +.PHONY: lint-fix +lint-fix: ## Auto-fix lint and format + uv run ruff check . --fix + uv run ruff format . + +.PHONY: typecheck +typecheck: ## Run mypy type checks + uv run mypy + +.PHONY: test +test: ## Run test suite + uv run pytest + +.PHONY: secrets +secrets: ## Run gitleaks secret scan + uv run pre-commit run gitleaks --all-files + +.PHONY: check +check: ## Run full local CI checks + @$(MAKE) lint + @$(MAKE) typecheck + @$(MAKE) test + @$(MAKE) secrets diff --git a/README.md b/README.md index f763676..de53ff0 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,14 @@ Define your datasources, pipes, and queries in Python and sync them directly to pip install tinybird-sdk ``` +## Development + +```bash +uv sync --group dev +uv run pre-commit install +make check +``` + ## Requirements - Python `>=3.11` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..38e6c31 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,22 @@ +# Security Policy + +## Supported Versions + +Security fixes are applied to the latest released version. + +## Reporting a Vulnerability + +Please report suspected vulnerabilities privately by emailing: + +- support@tinybird.co + +Do not open public issues for security vulnerabilities. + +When reporting, include: + +- A clear description of the issue +- Impact assessment +- Reproduction steps or proof of concept +- Any suggested remediation + +We will acknowledge receipt as soon as possible and follow up with remediation status. diff --git a/pyproject.toml b/pyproject.toml index 92d35be..4b5a4e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,7 @@ name = "tinybird-sdk" version = "0.1.9" description = "Python SDK for Tinybird Forward" readme = "README.md" +license = "MIT" authors = [ { name = "Tinybird", email = "support@tinybird.co" } ] @@ -10,6 +11,21 @@ requires-python = ">=3.11" dependencies = [ "tinybird==4.5.0", ] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries", +] + +[project.urls] +Homepage = "https://github.com/tinybirdco/tinybird-sdk-python" +Repository = "https://github.com/tinybirdco/tinybird-sdk-python" +Issues = "https://github.com/tinybirdco/tinybird-sdk-python/issues" +Changelog = "https://github.com/tinybirdco/tinybird-sdk-python/blob/main/CHANGELOG.md" [project.scripts] tinybird = "tinybird_sdk.cli.index:main" @@ -23,7 +39,33 @@ pythonpath = ["src"] addopts = "-q" testpaths = ["tests"] +[tool.ruff] +target-version = "py311" +line-length = 100 +src = ["src", "tests"] +extend-exclude = ["tests/fixtures/**"] + +[tool.ruff.lint] +select = ["E", "F"] +ignore = ["E501", "F401"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" + +[tool.mypy] +python_version = "3.11" +files = ["src"] +warn_unused_configs = true +warn_redundant_casts = true +warn_unused_ignores = true +pretty = true +ignore_missing_imports = true + [dependency-groups] dev = [ + "mypy>=1.18.2", + "pre-commit>=4.3.0", "pytest>=9.0.2", + "ruff>=0.13.2", ] diff --git a/src/tinybird_sdk/_http.py b/src/tinybird_sdk/_http.py index 7919fe2..20a60f6 100644 --- a/src/tinybird_sdk/_http.py +++ b/src/tinybird_sdk/_http.py @@ -157,7 +157,9 @@ def create_multipart_body( ) for field_name, filename, content, explicit_content_type in files: - content_type = explicit_content_type or mimetypes.guess_type(filename)[0] or "application/octet-stream" + content_type = ( + explicit_content_type or mimetypes.guess_type(filename)[0] or "application/octet-stream" + ) lines.extend( [ f"--{boundary}".encode(), diff --git a/src/tinybird_sdk/api/__init__.py b/src/tinybird_sdk/api/__init__.py index 8563105..799ffbe 100644 --- a/src/tinybird_sdk/api/__init__.py +++ b/src/tinybird_sdk/api/__init__.py @@ -24,7 +24,13 @@ clear_branch, ) from .workspaces import TinybirdWorkspace, WorkspaceApiConfig, WorkspaceApiError, get_workspace -from .dashboard import parse_api_url, get_dashboard_url, get_branch_dashboard_url, get_local_dashboard_url, RegionInfo +from .dashboard import ( + parse_api_url, + get_dashboard_url, + get_branch_dashboard_url, + get_local_dashboard_url, + RegionInfo, +) from .tokens import create_jwt, TokenApiConfig, TokenApiError from .resources import ( ResourceApiError, diff --git a/src/tinybird_sdk/api/api.py b/src/tinybird_sdk/api/api.py index f47f68c..8ee18b5 100644 --- a/src/tinybird_sdk/api/api.py +++ b/src/tinybird_sdk/api/api.py @@ -47,7 +47,9 @@ def __init__( class TinybirdApi: def __init__(self, config: TinybirdApiConfig | dict[str, Any]): - normalized = config if isinstance(config, TinybirdApiConfig) else TinybirdApiConfig(**config) + normalized = ( + config if isinstance(config, TinybirdApiConfig) else TinybirdApiConfig(**config) + ) if not normalized.base_url: raise ValueError("base_url is required") @@ -185,13 +187,17 @@ def ingest_batch( if response.ok: return response.json() - retry_429_delay_ms = self._resolve_retry_429_delay_ms(response.status_code, response.headers, max_retries, retry_count) + retry_429_delay_ms = self._resolve_retry_429_delay_ms( + response.status_code, response.headers, max_retries, retry_count + ) if retry_429_delay_ms is not None: self._sleep_ms(retry_429_delay_ms) retry_count += 1 continue - retry_503_delay_ms = self._resolve_retry_503_delay_ms(response.status_code, max_retries, retry_count) + retry_503_delay_ms = self._resolve_retry_503_delay_ms( + response.status_code, max_retries, retry_count + ) if retry_503_delay_ms is not None: self._sleep_ms(retry_503_delay_ms) retry_count += 1 @@ -233,7 +239,10 @@ def append_datasource( "mode": api_options.get("mode", "append"), } - detected_format = detect_data_format(source_url or file_path) + source_url_str = source_url if isinstance(source_url, str) else None + file_path_str = file_path if isinstance(file_path, str) else None + source_ref = source_url_str or file_path_str + detected_format = detect_data_format(source_ref) if source_ref else None if detected_format: query["format"] = detected_format @@ -247,8 +256,8 @@ def append_datasource( timeout = options.get("timeout", api_options.get("timeout")) - if source_url: - body = urlencode({"url": source_url}) + if source_url_str: + body = urlencode({"url": source_url_str}) response = self.request( f"/v0/datasources?{urlencode(query)}", method="POST", @@ -258,10 +267,12 @@ def append_datasource( timeout=timeout, ) else: - with open(file_path, "rb") as fp: + if not file_path_str: + raise ValueError("'file' must be a valid string path") + with open(file_path_str, "rb") as fp: file_content = fp.read() content_type, multipart = create_multipart_body( - files=[("csv", file_path, file_content, None)], + files=[("csv", file_path_str, file_content, None)], ) response = self.request( f"/v0/datasources?{urlencode(query)}", @@ -370,7 +381,11 @@ def _resolve_ingest_max_retries(self, options: dict[str, Any]) -> int | None: value = options.get("max_retries") if value is None: return None - if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value): + if ( + isinstance(value, bool) + or not isinstance(value, (int, float)) + or not math.isfinite(value) + ): raise ValueError("'maxRetries' must be a finite number") return max(0, math.floor(value)) @@ -385,7 +400,9 @@ def _resolve_retry_429_delay_ms( return None return self._resolve_retry_delay_from_headers(headers) - def _resolve_retry_503_delay_ms(self, status_code: int, max_retries: int | None, retry_count: int) -> int | None: + def _resolve_retry_503_delay_ms( + self, status_code: int, max_retries: int | None, retry_count: int + ) -> int | None: if max_retries is None or status_code != 503 or retry_count >= max_retries: return None return self._calculate_retry_503_delay_ms(retry_count) @@ -410,7 +427,11 @@ def _get_header(self, headers: dict[str, str] | Any, header_name: str) -> str | return value for key, value in dict(headers).items(): - if isinstance(key, str) and key.lower() == header_name.lower() and isinstance(value, str): + if ( + isinstance(key, str) + and key.lower() == header_name.lower() + and isinstance(value, str) + ): return value return None diff --git a/src/tinybird_sdk/api/branches.py b/src/tinybird_sdk/api/branches.py index 2244ce3..64f19e8 100644 --- a/src/tinybird_sdk/api/branches.py +++ b/src/tinybird_sdk/api/branches.py @@ -8,6 +8,8 @@ from .fetcher import tinybird_fetch +LAST_PARTITION = "last_partition" + @dataclass(frozen=True, slots=True) class BranchApiConfig: @@ -34,7 +36,9 @@ def _headers(token: str) -> dict[str, str]: return {"Authorization": f"Bearer {token}"} -def _poll_job(config: BranchApiConfig, job_id: str, max_attempts: int = 120, interval_ms: int = 1000) -> None: +def _poll_job( + config: BranchApiConfig, job_id: str, max_attempts: int = 120, interval_ms: int = 1000 +) -> None: for _ in range(max_attempts): response = tinybird_fetch( f"{config.base_url.rstrip('/')}/v0/jobs/{job_id}", @@ -80,13 +84,17 @@ def create_branch(config: BranchApiConfig | dict[str, Any], name: str) -> Tinybi elif response.status_code == 409: message = f"Branch '{name}' already exists." else: - message = f"Failed to create branch '{name}': {response.status_code}. API response: {body}" + message = ( + f"Failed to create branch '{name}': {response.status_code}. API response: {body}" + ) raise BranchApiError(message, response.status_code, body) job = response.json().get("job", {}) job_id = job.get("id") if not job_id: - raise BranchApiError("Unexpected response from branch creation: no job ID returned", 500, response.json()) + raise BranchApiError( + "Unexpected response from branch creation: no job ID returned", 500, response.json() + ) _poll_job(normalized, job_id) return get_branch(normalized, name) diff --git a/src/tinybird_sdk/api/build.py b/src/tinybird_sdk/api/build.py index 7982a16..c560973 100644 --- a/src/tinybird_sdk/api/build.py +++ b/src/tinybird_sdk/api/build.py @@ -31,11 +31,22 @@ def build_to_tinybird( files: list[tuple[str, str, bytes, str | None]] = [] for ds in resources.datasources: - files.append(("data_project://", f"{ds.name}.datasource", ds.content.encode("utf-8"), "text/plain")) + files.append( + ("data_project://", f"{ds.name}.datasource", ds.content.encode("utf-8"), "text/plain") + ) for pipe in resources.pipes: - files.append(("data_project://", f"{pipe.name}.pipe", pipe.content.encode("utf-8"), "text/plain")) + files.append( + ("data_project://", f"{pipe.name}.pipe", pipe.content.encode("utf-8"), "text/plain") + ) for conn in resources.connections: - files.append(("data_project://", f"{conn.name}.connection", conn.content.encode("utf-8"), "text/plain")) + files.append( + ( + "data_project://", + f"{conn.name}.connection", + conn.content.encode("utf-8"), + "text/plain", + ) + ) content_type, body = create_multipart_body(files=files) url = f"{normalized.base_url.rstrip('/')}/v1/build" diff --git a/src/tinybird_sdk/api/deploy.py b/src/tinybird_sdk/api/deploy.py index 83452d6..90d32cf 100644 --- a/src/tinybird_sdk/api/deploy.py +++ b/src/tinybird_sdk/api/deploy.py @@ -42,7 +42,9 @@ def deploy_to_main( files: list[tuple[str, str, bytes, str | None]] = [] for ds in resources.datasources: - files.append(("data_project://", f"{ds.name}.datasource", ds.content.encode(), "text/plain")) + files.append( + ("data_project://", f"{ds.name}.datasource", ds.content.encode(), "text/plain") + ) for pipe in resources.pipes: files.append(("data_project://", f"{pipe.name}.pipe", pipe.content.encode(), "text/plain")) @@ -87,7 +89,9 @@ def _format_errors() -> str: api_errors = parsed.get("errors") or [] if api_errors: return "\n".join( - f"[{item.get('filename')}] {item.get('error')}" if item.get("filename") else str(item.get("error")) + f"[{item.get('filename')}] {item.get('error')}" + if item.get("filename") + else str(item.get("error")) for item in api_errors ) if parsed.get("error"): diff --git a/src/tinybird_sdk/api/local.py b/src/tinybird_sdk/api/local.py index f18e08a..0d4a155 100644 --- a/src/tinybird_sdk/api/local.py +++ b/src/tinybird_sdk/api/local.py @@ -46,10 +46,20 @@ def get_local_tokens() -> LocalTokens: try: response = tinybird_fetch(f"{LOCAL_BASE_URL}/tokens", method="GET", timeout=5) if not response.ok: - raise LocalApiError(f"Failed to get local tokens: {response.status_code}", response.status_code, response.text) + raise LocalApiError( + f"Failed to get local tokens: {response.status_code}", + response.status_code, + response.text, + ) data = response.json() - if not data.get("user_token") or not data.get("admin_token") or not data.get("workspace_admin_token"): - raise LocalApiError("Invalid tokens response from local Tinybird - missing required fields") + if ( + not data.get("user_token") + or not data.get("admin_token") + or not data.get("workspace_admin_token") + ): + raise LocalApiError( + "Invalid tokens response from local Tinybird - missing required fields" + ) return LocalTokens(**data) except LocalApiError: raise @@ -64,7 +74,11 @@ def list_local_workspaces(admin_token: str) -> dict[str, Any]: query = urlencode({"with_organization": "true", "token": admin_token}) response = tinybird_fetch(f"{LOCAL_BASE_URL}/v1/user/workspaces?{query}", method="GET") if not response.ok: - raise LocalApiError(f"Failed to list local workspaces: {response.status_code}", response.status_code, response.text) + raise LocalApiError( + f"Failed to list local workspaces: {response.status_code}", + response.status_code, + response.text, + ) data = response.json() return { "workspaces": [LocalWorkspace(**workspace) for workspace in data.get("workspaces", [])], @@ -72,7 +86,9 @@ def list_local_workspaces(admin_token: str) -> dict[str, Any]: } -def create_local_workspace(user_token: str, workspace_name: str, organization_id: str | None = None) -> LocalWorkspace: +def create_local_workspace( + user_token: str, workspace_name: str, organization_id: str | None = None +) -> LocalWorkspace: body = {"name": workspace_name} if organization_id: body["assign_to_organization_id"] = organization_id @@ -99,7 +115,9 @@ def get_or_create_local_workspace(tokens: LocalTokens, workspace_name: str) -> d listed = list_local_workspaces(tokens.admin_token) workspaces: list[LocalWorkspace] = listed["workspaces"] - existing = next((workspace for workspace in workspaces if workspace.name == workspace_name), None) + existing = next( + (workspace for workspace in workspaces if workspace.name == workspace_name), None + ) if existing: return {"workspace": existing, "was_created": False} @@ -108,7 +126,9 @@ def get_or_create_local_workspace(tokens: LocalTokens, workspace_name: str) -> d refreshed = list_local_workspaces(tokens.admin_token)["workspaces"] created = next((workspace for workspace in refreshed if workspace.name == workspace_name), None) if not created: - raise LocalApiError(f"Created workspace '{workspace_name}' but could not find it in workspace list") + raise LocalApiError( + f"Created workspace '{workspace_name}' but could not find it in workspace list" + ) return {"workspace": created, "was_created": True} @@ -139,7 +159,9 @@ def clear_local_workspace(tokens: LocalTokens, workspace_name: str) -> LocalWork listed = list_local_workspaces(tokens.admin_token) workspaces: list[LocalWorkspace] = listed["workspaces"] - current = next((workspace for workspace in workspaces if workspace.name == workspace_name), None) + current = next( + (workspace for workspace in workspaces if workspace.name == workspace_name), None + ) if not current: raise LocalApiError(f"Workspace '{workspace_name}' not found") @@ -147,8 +169,12 @@ def clear_local_workspace(tokens: LocalTokens, workspace_name: str) -> LocalWork create_local_workspace(tokens.user_token, workspace_name, listed.get("organization_id")) refreshed = list_local_workspaces(tokens.admin_token)["workspaces"] - recreated = next((workspace for workspace in refreshed if workspace.name == workspace_name), None) + recreated = next( + (workspace for workspace in refreshed if workspace.name == workspace_name), None + ) if not recreated: - raise LocalApiError(f"Workspace '{workspace_name}' was not recreated properly. Please try again.") + raise LocalApiError( + f"Workspace '{workspace_name}' was not recreated properly. Please try again." + ) return recreated diff --git a/src/tinybird_sdk/api/resources.py b/src/tinybird_sdk/api/resources.py index fdaf469..45a929d 100644 --- a/src/tinybird_sdk/api/resources.py +++ b/src/tinybird_sdk/api/resources.py @@ -163,7 +163,9 @@ def _fetch_text_from_any_endpoint(config: WorkspaceApiConfig, endpoints: list[st continue return _handle_text_response(response, endpoint) - raise last_not_found or ResourceApiError("Resource not found", 404, endpoints[0] if endpoints else "unknown") + raise last_not_found or ResourceApiError( + "Resource not found", 404, endpoints[0] if endpoints else "unknown" + ) def list_datasources(config: WorkspaceApiConfig | dict[str, Any]) -> list[str]: @@ -205,9 +207,15 @@ def get_datasource(config: WorkspaceApiConfig | dict[str, Any], name: str) -> Da ], engine=DatasourceEngine( type=_parse_engine_type(engine.get("engine")), - sorting_key=engine.get("sorting_key") or engine.get("engine_sorting_key") or payload.get("sorting_key"), - partition_key=engine.get("partition_key") or engine.get("engine_partition_key") or payload.get("partition_key"), - primary_key=engine.get("primary_key") or engine.get("engine_primary_key") or payload.get("primary_key"), + sorting_key=engine.get("sorting_key") + or engine.get("engine_sorting_key") + or payload.get("sorting_key"), + partition_key=engine.get("partition_key") + or engine.get("engine_partition_key") + or payload.get("partition_key"), + primary_key=engine.get("primary_key") + or engine.get("engine_primary_key") + or payload.get("primary_key"), ttl=payload.get("ttl"), ver=engine.get("engine_ver"), sign=engine.get("engine_sign"), @@ -371,7 +379,9 @@ def fetch_all_resources(config: WorkspaceApiConfig | dict[str, Any]) -> dict[str } -def pull_all_resource_files(config: WorkspaceApiConfig | dict[str, Any]) -> dict[str, list[ResourceFile]]: +def pull_all_resource_files( + config: WorkspaceApiConfig | dict[str, Any], +) -> dict[str, list[ResourceFile]]: normalized = config if isinstance(config, WorkspaceApiConfig) else WorkspaceApiConfig(**config) datasource_names = list_datasources(normalized) pipe_names = list_pipes_v1(normalized) diff --git a/src/tinybird_sdk/api/tokens.py b/src/tinybird_sdk/api/tokens.py index b8f6d33..b7f798d 100644 --- a/src/tinybird_sdk/api/tokens.py +++ b/src/tinybird_sdk/api/tokens.py @@ -62,6 +62,8 @@ def create_jwt(config: TokenApiConfig | dict[str, Any], options: dict[str, Any]) elif error.status_code == 400: message = f"Invalid JWT token request: {response_body}" else: - message = f"Failed to create JWT token: {error.status_code}. API response: {response_body}" + message = ( + f"Failed to create JWT token: {error.status_code}. API response: {response_body}" + ) raise TokenApiError(message, error.status_code, response_body) from error diff --git a/src/tinybird_sdk/cli/__init__.py b/src/tinybird_sdk/cli/__init__.py index 0a35ba5..b2336e3 100644 --- a/src/tinybird_sdk/cli/__init__.py +++ b/src/tinybird_sdk/cli/__init__.py @@ -51,7 +51,11 @@ get_tinybird_branch_name, ) from .output import ResourceChange, output -from .region_selector import RegionSelectionResult, get_api_host_with_region_selection, select_region +from .region_selector import ( + RegionSelectionResult, + get_api_host_with_region_selection, + select_region, +) __all__ = [ "load_config", diff --git a/src/tinybird_sdk/cli/auth.py b/src/tinybird_sdk/cli/auth.py index 897d0a9..c0f40a2 100644 --- a/src/tinybird_sdk/cli/auth.py +++ b/src/tinybird_sdk/cli/auth.py @@ -73,7 +73,7 @@ def browser_login(options: dict[str, str] | None = None) -> AuthResult: done = Event() class Handler(BaseHTTPRequestHandler): - def do_GET(self): # type: ignore[override] + def do_GET(self): parsed = urlparse(self.path) query = parse_qs(parsed.query) code = (query.get("code") or [None])[0] @@ -108,7 +108,9 @@ def serve() -> None: if not done.wait(timeout=SERVER_MAX_WAIT_TIME): server.server_close() - return AuthResult(success=False, error=f"Authentication timed out after {SERVER_MAX_WAIT_TIME} seconds") + return AuthResult( + success=False, error=f"Authentication timed out after {SERVER_MAX_WAIT_TIME} seconds" + ) server.server_close() code = code_holder.get("code") diff --git a/src/tinybird_sdk/cli/branch_store.py b/src/tinybird_sdk/cli/branch_store.py index 98b23ed..bcff400 100644 --- a/src/tinybird_sdk/cli/branch_store.py +++ b/src/tinybird_sdk/cli/branch_store.py @@ -42,14 +42,22 @@ def save_branch_store(store: dict[str, Any]) -> None: def get_branch_token(workspace_id: str, branch_name: str) -> BranchInfo | None: store = load_branch_store() - info = (((store.get("workspaces") or {}).get(workspace_id) or {}).get("branches") or {}).get(branch_name) + info = (((store.get("workspaces") or {}).get(workspace_id) or {}).get("branches") or {}).get( + branch_name + ) if not info: return None return BranchInfo(id=info["id"], token=info["token"], created_at=info["created_at"]) -def set_branch_token(workspace_id: str, branch_name: str, info: BranchInfo | dict[str, str]) -> None: - payload = info if isinstance(info, dict) else {"id": info.id, "token": info.token, "created_at": info.created_at} +def set_branch_token( + workspace_id: str, branch_name: str, info: BranchInfo | dict[str, str] +) -> None: + payload = ( + info + if isinstance(info, dict) + else {"id": info.id, "token": info.token, "created_at": info.created_at} + ) store = load_branch_store() workspaces = store.setdefault("workspaces", {}) @@ -62,7 +70,7 @@ def set_branch_token(workspace_id: str, branch_name: str, info: BranchInfo | dic def remove_branch(workspace_id: str, branch_name: str) -> None: store = load_branch_store() workspaces = store.get("workspaces") or {} - branches = ((workspaces.get(workspace_id) or {}).get("branches") or {}) + branches = (workspaces.get(workspace_id) or {}).get("branches") or {} if branch_name in branches: del branches[branch_name] save_branch_store(store) @@ -70,7 +78,7 @@ def remove_branch(workspace_id: str, branch_name: str) -> None: def list_cached_branches(workspace_id: str) -> dict[str, BranchInfo]: store = load_branch_store() - branches = (((store.get("workspaces") or {}).get(workspace_id) or {}).get("branches") or {}) + branches = ((store.get("workspaces") or {}).get(workspace_id) or {}).get("branches") or {} result: dict[str, BranchInfo] = {} for name, info in branches.items(): result[name] = BranchInfo(id=info["id"], token=info["token"], created_at=info["created_at"]) diff --git a/src/tinybird_sdk/cli/commands/branch.py b/src/tinybird_sdk/cli/commands/branch.py index 1ca3c50..d04aa12 100644 --- a/src/tinybird_sdk/cli/commands/branch.py +++ b/src/tinybird_sdk/cli/commands/branch.py @@ -37,8 +37,14 @@ class BranchDeleteResult: error: str | None = None -def run_branch_list(options: BranchCommandOptions | dict[str, Any] | None = None) -> BranchListResult: - normalized = options if isinstance(options, BranchCommandOptions) else BranchCommandOptions(**(options or {})) +def run_branch_list( + options: BranchCommandOptions | dict[str, Any] | None = None, +) -> BranchListResult: + normalized = ( + options + if isinstance(options, BranchCommandOptions) + else BranchCommandOptions(**(options or {})) + ) try: config = load_config_async(normalized.cwd or os.getcwd()) branches = list_branches({"base_url": config["base_url"], "token": config["token"]}) @@ -47,21 +53,35 @@ def run_branch_list(options: BranchCommandOptions | dict[str, Any] | None = None return BranchListResult(success=False, branches=[], error=str(error)) -def run_branch_status(branch_name: str | None = None, options: BranchCommandOptions | dict[str, Any] | None = None) -> BranchStatusResult: - normalized = options if isinstance(options, BranchCommandOptions) else BranchCommandOptions(**(options or {})) +def run_branch_status( + branch_name: str | None = None, options: BranchCommandOptions | dict[str, Any] | None = None +) -> BranchStatusResult: + normalized = ( + options + if isinstance(options, BranchCommandOptions) + else BranchCommandOptions(**(options or {})) + ) try: config = load_config_async(normalized.cwd or os.getcwd()) target = branch_name or config.get("tinybird_branch") if not target: - return BranchStatusResult(success=False, error="No branch name provided and no tinybird_branch detected") + return BranchStatusResult( + success=False, error="No branch name provided and no tinybird_branch detected" + ) branch = get_branch({"base_url": config["base_url"], "token": config["token"]}, target) return BranchStatusResult(success=True, branch=asdict(branch)) except Exception as error: return BranchStatusResult(success=False, error=str(error)) -def run_branch_delete(branch_name: str, options: BranchCommandOptions | dict[str, Any] | None = None) -> BranchDeleteResult: - normalized = options if isinstance(options, BranchCommandOptions) else BranchCommandOptions(**(options or {})) +def run_branch_delete( + branch_name: str, options: BranchCommandOptions | dict[str, Any] | None = None +) -> BranchDeleteResult: + normalized = ( + options + if isinstance(options, BranchCommandOptions) + else BranchCommandOptions(**(options or {})) + ) try: config = load_config_async(normalized.cwd or os.getcwd()) delete_branch({"base_url": config["base_url"], "token": config["token"]}, branch_name) @@ -77,8 +97,14 @@ def run_branch_delete(branch_name: str, options: BranchCommandOptions | dict[str return BranchDeleteResult(success=False, deleted=False, error=str(error)) -def run_branch_list_cached(options: BranchCommandOptions | dict[str, Any] | None = None) -> BranchListResult: - normalized = options if isinstance(options, BranchCommandOptions) else BranchCommandOptions(**(options or {})) +def run_branch_list_cached( + options: BranchCommandOptions | dict[str, Any] | None = None, +) -> BranchListResult: + normalized = ( + options + if isinstance(options, BranchCommandOptions) + else BranchCommandOptions(**(options or {})) + ) try: config = load_config_async(normalized.cwd or os.getcwd()) workspace = get_workspace({"base_url": config["base_url"], "token": config["token"]}) diff --git a/src/tinybird_sdk/cli/commands/build.py b/src/tinybird_sdk/cli/commands/build.py index 94abeed..457a6d3 100644 --- a/src/tinybird_sdk/cli/commands/build.py +++ b/src/tinybird_sdk/cli/commands/build.py @@ -8,7 +8,12 @@ from ...api.branches import get_or_create_branch from ...api.build import build_to_tinybird from ...api.dashboard import get_branch_dashboard_url, get_local_dashboard_url -from ...api.local import LocalNotRunningError, get_local_tokens, get_local_workspace_name, get_or_create_local_workspace +from ...api.local import ( + LocalNotRunningError, + get_local_tokens, + get_local_workspace_name, + get_or_create_local_workspace, +) from ...api.workspaces import get_workspace from ...cli.config import LOCAL_BASE_URL, load_config_async from ...generator.index import BuildFromIncludeResult, build_from_include @@ -43,16 +48,24 @@ class BuildCommandResult: def run_build(options: BuildCommandOptions | dict[str, Any] | None = None) -> BuildCommandResult: start = int(time.time() * 1000) - normalized = options if isinstance(options, BuildCommandOptions) else BuildCommandOptions(**(options or {})) + normalized = ( + options + if isinstance(options, BuildCommandOptions) + else BuildCommandOptions(**(options or {})) + ) cwd = normalized.cwd or os.getcwd() try: config = load_config_async(cwd) except Exception as error: - return BuildCommandResult(success=False, error=str(error), duration_ms=int(time.time() * 1000) - start) + return BuildCommandResult( + success=False, error=str(error), duration_ms=int(time.time() * 1000) - start + ) try: - build_result = build_from_include({"include_paths": config["include"], "cwd": config["cwd"]}) + build_result = build_from_include( + {"include_paths": config["include"], "cwd": config["cwd"]} + ) except Exception as error: return BuildCommandResult( success=False, @@ -61,7 +74,9 @@ def run_build(options: BuildCommandOptions | dict[str, Any] | None = None) -> Bu ) if normalized.dry_run: - return BuildCommandResult(success=True, build=build_result, duration_ms=int(time.time() * 1000) - start) + return BuildCommandResult( + success=True, build=build_result, duration_ms=int(time.time() * 1000) - start + ) dev_mode = normalized.dev_mode_override or config.get("dev_mode") branch_info: BuildBranchInfo | None = None @@ -70,10 +85,14 @@ def run_build(options: BuildCommandOptions | dict[str, Any] | None = None) -> Bu try: local_tokens = get_local_tokens() if config.get("is_main_branch") or not config.get("tinybird_branch"): - workspace = get_workspace({"base_url": config["base_url"], "token": config["token"]}) + workspace = get_workspace( + {"base_url": config["base_url"], "token": config["token"]} + ) workspace_name = workspace.name else: - workspace_name = get_local_workspace_name(config.get("tinybird_branch"), config["cwd"]) + workspace_name = get_local_workspace_name( + config.get("tinybird_branch"), config["cwd"] + ) local_workspace = get_or_create_local_workspace(local_tokens, workspace_name) workspace_payload = local_workspace["workspace"] @@ -132,8 +151,12 @@ def run_build(options: BuildCommandOptions | dict[str, Any] | None = None) -> Bu ) effective_token = branch["token"] - workspace = get_workspace({"base_url": config["base_url"], "token": config["token"]}) - dashboard_url = get_branch_dashboard_url(config["base_url"], workspace.name, config["tinybird_branch"]) + workspace = get_workspace( + {"base_url": config["base_url"], "token": config["token"]} + ) + dashboard_url = get_branch_dashboard_url( + config["base_url"], workspace.name, config["tinybird_branch"] + ) branch_info = BuildBranchInfo( git_branch=config.get("git_branch"), diff --git a/src/tinybird_sdk/cli/commands/clear.py b/src/tinybird_sdk/cli/commands/clear.py index 2a3f081..176a446 100644 --- a/src/tinybird_sdk/cli/commands/clear.py +++ b/src/tinybird_sdk/cli/commands/clear.py @@ -27,12 +27,18 @@ class ClearResult: def run_clear(options: ClearCommandOptions | dict[str, Any] | None = None) -> ClearResult: start = int(time.time() * 1000) - normalized = options if isinstance(options, ClearCommandOptions) else ClearCommandOptions(**(options or {})) + normalized = ( + options + if isinstance(options, ClearCommandOptions) + else ClearCommandOptions(**(options or {})) + ) try: config = load_config_async(normalized.cwd or os.getcwd()) except Exception as error: - return ClearResult(success=False, error=str(error), duration_ms=int(time.time() * 1000) - start) + return ClearResult( + success=False, error=str(error), duration_ms=int(time.time() * 1000) - start + ) dev_mode = normalized.dev_mode_override or config.get("dev_mode", "branch") @@ -54,14 +60,18 @@ def run_clear(options: ClearCommandOptions | dict[str, Any] | None = None) -> Cl duration_ms=int(time.time() * 1000) - start, ) - clear_branch({"base_url": config["base_url"], "token": config["token"]}, config["tinybird_branch"]) + clear_branch( + {"base_url": config["base_url"], "token": config["token"]}, config["tinybird_branch"] + ) return ClearResult( success=True, branch=config["tinybird_branch"], duration_ms=int(time.time() * 1000) - start, ) except Exception as error: - return ClearResult(success=False, error=str(error), duration_ms=int(time.time() * 1000) - start) + return ClearResult( + success=False, error=str(error), duration_ms=int(time.time() * 1000) - start + ) __all__ = ["ClearCommandOptions", "ClearResult", "run_clear"] diff --git a/src/tinybird_sdk/cli/commands/deploy.py b/src/tinybird_sdk/cli/commands/deploy.py index d34dbb6..c2a9326 100644 --- a/src/tinybird_sdk/cli/commands/deploy.py +++ b/src/tinybird_sdk/cli/commands/deploy.py @@ -28,16 +28,24 @@ class DeployCommandResult: def run_deploy(options: DeployCommandOptions | dict[str, Any] | None = None) -> DeployCommandResult: start = int(time.time() * 1000) - normalized = options if isinstance(options, DeployCommandOptions) else DeployCommandOptions(**(options or {})) + normalized = ( + options + if isinstance(options, DeployCommandOptions) + else DeployCommandOptions(**(options or {})) + ) cwd = normalized.cwd or os.getcwd() try: config = load_config_async(cwd) except Exception as error: - return DeployCommandResult(success=False, error=str(error), duration_ms=int(time.time() * 1000) - start) + return DeployCommandResult( + success=False, error=str(error), duration_ms=int(time.time() * 1000) - start + ) try: - build_result = build_from_include({"include_paths": config["include"], "cwd": config["cwd"]}) + build_result = build_from_include( + {"include_paths": config["include"], "cwd": config["cwd"]} + ) except Exception as error: return DeployCommandResult( success=False, @@ -49,7 +57,10 @@ def run_deploy(options: DeployCommandOptions | dict[str, Any] | None = None) -> deploy_result = deploy_to_main( {"base_url": config["base_url"], "token": config["token"]}, build_result.resources, - {"check": normalized.check, "allow_destructive_operations": normalized.allow_destructive_operations}, + { + "check": normalized.check, + "allow_destructive_operations": normalized.allow_destructive_operations, + }, ) except Exception as error: return DeployCommandResult( diff --git a/src/tinybird_sdk/cli/commands/dev.py b/src/tinybird_sdk/cli/commands/dev.py index 2ed1318..250b366 100644 --- a/src/tinybird_sdk/cli/commands/dev.py +++ b/src/tinybird_sdk/cli/commands/dev.py @@ -39,7 +39,9 @@ class DevController: def run_dev(options: DevCommandOptions | dict[str, Any] | None = None) -> dict[str, Any]: - normalized = options if isinstance(options, DevCommandOptions) else DevCommandOptions(**(options or {})) + normalized = ( + options if isinstance(options, DevCommandOptions) else DevCommandOptions(**(options or {})) + ) cwd = normalized.cwd or os.getcwd() config = load_config_async(cwd) diff --git a/src/tinybird_sdk/cli/commands/generate.py b/src/tinybird_sdk/cli/commands/generate.py index 2e9dcc5..1f78ab3 100644 --- a/src/tinybird_sdk/cli/commands/generate.py +++ b/src/tinybird_sdk/cli/commands/generate.py @@ -81,14 +81,22 @@ def _write_artifacts(output_dir: Path, artifacts: list[GeneratedResourceArtifact target_path.write_text(artifact.content, encoding="utf-8") -def run_generate(options: GenerateCommandOptions | dict[str, Any] | None = None) -> GenerateCommandResult: +def run_generate( + options: GenerateCommandOptions | dict[str, Any] | None = None, +) -> GenerateCommandResult: start = int(time.time() * 1000) - normalized = options if isinstance(options, GenerateCommandOptions) else GenerateCommandOptions(**(options or {})) + normalized = ( + options + if isinstance(options, GenerateCommandOptions) + else GenerateCommandOptions(**(options or {})) + ) cwd = Path(normalized.cwd or os.getcwd()).resolve() try: config = load_config_async(str(cwd)) - build_result = build_from_include({"include_paths": config["include"], "cwd": config["cwd"]}) + build_result = build_from_include( + {"include_paths": config["include"], "cwd": config["cwd"]} + ) artifacts = _to_artifacts(build_result) resolved_output_dir: str | None = None @@ -119,6 +127,7 @@ def run_generate(options: GenerateCommandOptions | dict[str, Any] | None = None) duration_ms=int(time.time() * 1000) - start, ) + runGenerate = run_generate diff --git a/src/tinybird_sdk/cli/commands/info.py b/src/tinybird_sdk/cli/commands/info.py index 7d56a05..f6f7bfe 100644 --- a/src/tinybird_sdk/cli/commands/info.py +++ b/src/tinybird_sdk/cli/commands/info.py @@ -32,18 +32,26 @@ class InfoCommandResult: def run_info(options: InfoCommandOptions | dict[str, Any] | None = None) -> InfoCommandResult: - normalized = options if isinstance(options, InfoCommandOptions) else InfoCommandOptions(**(options or {})) + normalized = ( + options + if isinstance(options, InfoCommandOptions) + else InfoCommandOptions(**(options or {})) + ) cwd = normalized.cwd or os.getcwd() if not config_exists(cwd): - return InfoCommandResult(success=False, error="No Tinybird config found in current directory tree") + return InfoCommandResult( + success=False, error="No Tinybird config found in current directory tree" + ) try: config = load_config_async(cwd) workspace = get_workspace({"base_url": config["base_url"], "token": config["token"]}) branches = list_branches({"base_url": config["base_url"], "token": config["token"]}) - datasource_names = list_datasources({"base_url": config["base_url"], "token": config["token"]}) + datasource_names = list_datasources( + {"base_url": config["base_url"], "token": config["token"]} + ) pipe_names = list_pipes({"base_url": config["base_url"], "token": config["token"]}) project_info = { diff --git a/src/tinybird_sdk/cli/commands/init.py b/src/tinybird_sdk/cli/commands/init.py index 4da6927..7a52ca4 100644 --- a/src/tinybird_sdk/cli/commands/init.py +++ b/src/tinybird_sdk/cli/commands/init.py @@ -60,7 +60,7 @@ def _client_template(resources_import_path: str) -> str: - return f'''import os + return f"""import os from tinybird_sdk import Tinybird from {resources_import_path} import page_views, top_pages @@ -73,11 +73,11 @@ def _client_template(resources_import_path: str) -> str: "token": os.getenv("TINYBIRD_TOKEN"), }} ) -''' +""" def _main_template(client_import_path: str) -> str: - return f'''from datetime import datetime, timezone + return f"""from datetime import datetime, timezone from dotenv import load_dotenv @@ -114,7 +114,7 @@ def main(): if __name__ == "__main__": main() -''' +""" @dataclass(frozen=True, slots=True) @@ -150,7 +150,7 @@ def _write_file(path: Path, content: str, force: bool) -> None: def _run_tinybird_cli_init(argv: list[str]) -> int: try: - from tinybird.tb.cli import cli as upstream_cli # type: ignore[import-not-found] + from tinybird.tb.cli import cli as upstream_cli except ModuleNotFoundError: return 1 diff --git a/src/tinybird_sdk/cli/commands/login.py b/src/tinybird_sdk/cli/commands/login.py index 66f94a8..744effc 100644 --- a/src/tinybird_sdk/cli/commands/login.py +++ b/src/tinybird_sdk/cli/commands/login.py @@ -26,7 +26,9 @@ class LoginResult: def run_login(options: RunLoginOptions | dict[str, Any] | None = None) -> LoginResult: - normalized = options if isinstance(options, RunLoginOptions) else RunLoginOptions(**(options or {})) + normalized = ( + options if isinstance(options, RunLoginOptions) else RunLoginOptions(**(options or {})) + ) result = browser_login({"api_host": normalized.api_host} if normalized.api_host else {}) if not result.success: return LoginResult(success=False, error=result.error) diff --git a/src/tinybird_sdk/cli/commands/migrate.py b/src/tinybird_sdk/cli/commands/migrate.py index d9da880..426020f 100644 --- a/src/tinybird_sdk/cli/commands/migrate.py +++ b/src/tinybird_sdk/cli/commands/migrate.py @@ -17,7 +17,9 @@ class MigrateCommandOptions: def run_migrate_command(options: MigrateCommandOptions | dict[str, Any]) -> dict[str, Any]: - normalized = options if isinstance(options, MigrateCommandOptions) else MigrateCommandOptions(**options) + normalized = ( + options if isinstance(options, MigrateCommandOptions) else MigrateCommandOptions(**options) + ) result = run_migrate_runner( { "cwd": normalized.cwd, diff --git a/src/tinybird_sdk/cli/commands/open_dashboard.py b/src/tinybird_sdk/cli/commands/open_dashboard.py index fb2d99f..740baf6 100644 --- a/src/tinybird_sdk/cli/commands/open_dashboard.py +++ b/src/tinybird_sdk/cli/commands/open_dashboard.py @@ -28,28 +28,43 @@ class OpenDashboardCommandResult: error: str | None = None -def run_open_dashboard(options: OpenDashboardCommandOptions | dict[str, Any] | None = None) -> OpenDashboardCommandResult: - normalized = options if isinstance(options, OpenDashboardCommandOptions) else OpenDashboardCommandOptions(**(options or {})) +def run_open_dashboard( + options: OpenDashboardCommandOptions | dict[str, Any] | None = None, +) -> OpenDashboardCommandResult: + normalized = ( + options + if isinstance(options, OpenDashboardCommandOptions) + else OpenDashboardCommandOptions(**(options or {})) + ) try: config = load_config_async(normalized.cwd or os.getcwd()) workspace = get_workspace({"base_url": config["base_url"], "token": config["token"]}) env = normalized.environment or config.get("dev_mode") or "cloud" + url: str | None = None if env == "local": workspace_name = config.get("tinybird_branch") or workspace.name url = get_local_dashboard_url(workspace_name) elif env == "branch": branch_name = config.get("tinybird_branch") if not branch_name: - return OpenDashboardCommandResult(success=False, error="No tinybird branch available", environment="branch") + return OpenDashboardCommandResult( + success=False, error="No tinybird branch available", environment="branch" + ) url = get_branch_dashboard_url(config["base_url"], workspace.name, branch_name) if not url: - return OpenDashboardCommandResult(success=False, error="Could not derive branch dashboard URL", environment="branch") + return OpenDashboardCommandResult( + success=False, + error="Could not derive branch dashboard URL", + environment="branch", + ) else: url = get_dashboard_url(config["base_url"], workspace.name) if not url: - return OpenDashboardCommandResult(success=False, error="Could not derive dashboard URL", environment="cloud") + return OpenDashboardCommandResult( + success=False, error="Could not derive dashboard URL", environment="cloud" + ) opened = webbrowser.open(url) return OpenDashboardCommandResult( @@ -62,4 +77,9 @@ def run_open_dashboard(options: OpenDashboardCommandOptions | dict[str, Any] | N return OpenDashboardCommandResult(success=False, error=str(error)) -__all__ = ["Environment", "OpenDashboardCommandOptions", "OpenDashboardCommandResult", "run_open_dashboard"] +__all__ = [ + "Environment", + "OpenDashboardCommandOptions", + "OpenDashboardCommandResult", + "run_open_dashboard", +] diff --git a/src/tinybird_sdk/cli/commands/preview.py b/src/tinybird_sdk/cli/commands/preview.py index daf03bd..9a52a31 100644 --- a/src/tinybird_sdk/cli/commands/preview.py +++ b/src/tinybird_sdk/cli/commands/preview.py @@ -38,21 +38,31 @@ def generate_preview_branch_name(git_branch: str | None) -> str: return f"tmp_ci_{branch_part}" -def run_preview(options: PreviewCommandOptions | dict[str, Any] | None = None) -> PreviewCommandResult: +def run_preview( + options: PreviewCommandOptions | dict[str, Any] | None = None, +) -> PreviewCommandResult: start = int(time.time() * 1000) - normalized = options if isinstance(options, PreviewCommandOptions) else PreviewCommandOptions(**(options or {})) + normalized = ( + options + if isinstance(options, PreviewCommandOptions) + else PreviewCommandOptions(**(options or {})) + ) cwd = normalized.cwd or os.getcwd() try: config = load_config_async(cwd) except Exception as error: - return PreviewCommandResult(success=False, error=str(error), duration_ms=int(time.time() * 1000) - start) + return PreviewCommandResult( + success=False, error=str(error), duration_ms=int(time.time() * 1000) - start + ) git_branch = get_current_git_branch() preview_branch_name = normalized.name or generate_preview_branch_name(git_branch) try: - build_result = build_from_include({"include_paths": config["include"], "cwd": config["cwd"]}) + build_result = build_from_include( + {"include_paths": config["include"], "cwd": config["cwd"]} + ) except Exception as error: return PreviewCommandResult( success=False, @@ -119,19 +129,31 @@ def run_preview(options: PreviewCommandOptions | dict[str, Any] | None = None) - duration_ms=int(time.time() * 1000) - start, ) except LocalNotRunningError as error: - return PreviewCommandResult(success=False, error=str(error), duration_ms=int(time.time() * 1000) - start) + return PreviewCommandResult( + success=False, error=str(error), duration_ms=int(time.time() * 1000) - start + ) except Exception as error: - return PreviewCommandResult(success=False, error=f"Local preview failed: {error}", duration_ms=int(time.time() * 1000) - start) + return PreviewCommandResult( + success=False, + error=f"Local preview failed: {error}", + duration_ms=int(time.time() * 1000) - start, + ) try: try: - existing = get_branch({"base_url": config["base_url"], "token": config["token"]}, preview_branch_name) + existing = get_branch( + {"base_url": config["base_url"], "token": config["token"]}, preview_branch_name + ) if existing: - delete_branch({"base_url": config["base_url"], "token": config["token"]}, preview_branch_name) + delete_branch( + {"base_url": config["base_url"], "token": config["token"]}, preview_branch_name + ) except Exception: pass - branch = create_branch({"base_url": config["base_url"], "token": config["token"]}, preview_branch_name) + branch = create_branch( + {"base_url": config["base_url"], "token": config["token"]}, preview_branch_name + ) except Exception as error: return PreviewCommandResult( success=False, @@ -197,4 +219,9 @@ def run_preview(options: PreviewCommandOptions | dict[str, Any] | None = None) - ) -__all__ = ["PreviewCommandOptions", "PreviewCommandResult", "generate_preview_branch_name", "run_preview"] +__all__ = [ + "PreviewCommandOptions", + "PreviewCommandResult", + "generate_preview_branch_name", + "run_preview", +] diff --git a/src/tinybird_sdk/cli/commands/pull.py b/src/tinybird_sdk/cli/commands/pull.py index acf3157..bbe8658 100644 --- a/src/tinybird_sdk/cli/commands/pull.py +++ b/src/tinybird_sdk/cli/commands/pull.py @@ -47,7 +47,11 @@ def _flatten_resources(resources: dict[str, list[ResourceFile]]) -> list[Resourc def run_pull(options: PullCommandOptions | dict[str, Any] | None = None) -> PullCommandResult: start = int(time.time() * 1000) - normalized = options if isinstance(options, PullCommandOptions) else PullCommandOptions(**(options or {})) + normalized = ( + options + if isinstance(options, PullCommandOptions) + else PullCommandOptions(**(options or {})) + ) cwd = Path(normalized.cwd or os.getcwd()).resolve() output_dir = Path(normalized.output_dir) if not output_dir.is_absolute(): @@ -56,7 +60,9 @@ def run_pull(options: PullCommandOptions | dict[str, Any] | None = None) -> Pull try: config = load_config_async(str(cwd)) except Exception as error: - return PullCommandResult(success=False, error=str(error), duration_ms=int(time.time() * 1000) - start) + return PullCommandResult( + success=False, error=str(error), duration_ms=int(time.time() * 1000) - start + ) try: pulled = pull_all_resource_files({"base_url": config["base_url"], "token": config["token"]}) diff --git a/src/tinybird_sdk/cli/config.py b/src/tinybird_sdk/cli/config.py index 230204e..49a0b59 100644 --- a/src/tinybird_sdk/cli/config.py +++ b/src/tinybird_sdk/cli/config.py @@ -158,7 +158,11 @@ def _resolve_config(config: TinybirdConfig, config_path: str) -> ResolvedConfig: load_env_files(config_dir) tinyb_auth = _read_tinyb_auth(config_dir) - token = _try_interpolate_env_vars(config.token) or os.getenv("TINYBIRD_TOKEN") or tinyb_auth["token"] + token = ( + _try_interpolate_env_vars(config.token) + or os.getenv("TINYBIRD_TOKEN") + or tinyb_auth["token"] + ) if not token: raise ValueError( f"Missing Tinybird token in {config_path}. " @@ -251,7 +255,9 @@ def update_config(config_path: str, updates: dict[str, Any]) -> None: if not path.exists(): raise ValueError(f"Config not found at {config_path}") if path.suffix != ".json": - raise ValueError(f"Cannot update {config_path}. Only JSON config files can be updated programmatically.") + raise ValueError( + f"Cannot update {config_path}. Only JSON config files can be updated programmatically." + ) with open(path, "r", encoding="utf-8") as fp: current = json.load(fp) diff --git a/src/tinybird_sdk/cli/git.py b/src/tinybird_sdk/cli/git.py index cc222ec..68f66e2 100644 --- a/src/tinybird_sdk/cli/git.py +++ b/src/tinybird_sdk/cli/git.py @@ -31,7 +31,9 @@ def _get_branch_from_ci_env() -> str | None: def get_current_git_branch() -> str | None: try: branch = ( - subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], stderr=subprocess.DEVNULL) + subprocess.check_output( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], stderr=subprocess.DEVNULL + ) .decode("utf-8") .strip() ) @@ -58,7 +60,9 @@ def is_git_repo() -> bool: def get_git_root() -> str | None: try: return ( - subprocess.check_output(["git", "rev-parse", "--show-toplevel"], stderr=subprocess.DEVNULL) + subprocess.check_output( + ["git", "rev-parse", "--show-toplevel"], stderr=subprocess.DEVNULL + ) .decode("utf-8") .strip() ) diff --git a/src/tinybird_sdk/cli/index.py b/src/tinybird_sdk/cli/index.py index 50dcec5..1183961 100644 --- a/src/tinybird_sdk/cli/index.py +++ b/src/tinybird_sdk/cli/index.py @@ -26,7 +26,7 @@ def _exit_code_from_system_exit(error: SystemExit) -> int: def _run_installed_tinybird_cli(argv: list[str]) -> int: try: - from tinybird.tb.cli import cli as upstream_cli # type: ignore[import-not-found] + from tinybird.tb.cli import cli as upstream_cli except ModuleNotFoundError: output.error("Installed Tinybird CLI dependency is required but could not be imported.") return 1 @@ -45,20 +45,34 @@ def create_cli() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(prog="tinybird", description="Tinybird Python SDK CLI") sub = parser.add_subparsers(dest="command", required=True) - init_cmd = sub.add_parser("init", help="Initialize a new Tinybird project with Python SDK templates") + init_cmd = sub.add_parser( + "init", help="Initialize a new Tinybird project with Python SDK templates" + ) init_cmd.add_argument("--folder", help="Target folder for generated Python files") init_cmd.add_argument("--force", action="store_true", help="Overwrite existing files") - generate_cmd = sub.add_parser("generate", help="Generate Tinybird datafiles from Python definitions") + generate_cmd = sub.add_parser( + "generate", help="Generate Tinybird datafiles from Python definitions" + ) generate_cmd.add_argument("--json", action="store_true") generate_cmd.add_argument("-o", "--output-dir") - migrate_cmd = sub.add_parser("migrate", help="Migrate Tinybird .datasource/.pipe files to Python resources") - migrate_cmd.add_argument("patterns", nargs="+", help="Files, directories, or glob patterns to migrate") + migrate_cmd = sub.add_parser( + "migrate", help="Migrate Tinybird .datasource/.pipe files to Python resources" + ) + migrate_cmd.add_argument( + "patterns", nargs="+", help="Files, directories, or glob patterns to migrate" + ) migrate_cmd.add_argument("--cwd", help="Working directory to resolve patterns from") - migrate_cmd.add_argument("-o", "--out", help="Output file path for the generated migration module") - migrate_cmd.add_argument("--dry-run", action="store_true", help="Generate output without writing files") - migrate_cmd.add_argument("--force", action="store_true", help="Overwrite existing output file when needed") + migrate_cmd.add_argument( + "-o", "--out", help="Output file path for the generated migration module" + ) + migrate_cmd.add_argument( + "--dry-run", action="store_true", help="Generate output without writing files" + ) + migrate_cmd.add_argument( + "--force", action="store_true", help="Overwrite existing output file when needed" + ) migrate_cmd.add_argument( "--strict", action=argparse.BooleanOptionalAction, @@ -81,10 +95,12 @@ def main(argv: list[str] | None = None) -> int: args = parser.parse_args(normalized_argv) if args.command == "init": - result = run_init({ - "folder": args.folder, - "force": args.force, - }) + result = run_init( + { + "folder": args.folder, + "force": args.force, + } + ) if not result.success: output.error(result.error or "Init failed") return 1 @@ -99,16 +115,16 @@ def main(argv: list[str] | None = None) -> int: return 0 if args.command == "generate": - result = run_generate({"output_dir": args.output_dir}) - if not result.success: - output.error(result.error or "Generate failed") + generate_result = run_generate({"output_dir": args.output_dir}) + if not generate_result.success: + output.error(generate_result.error or "Generate failed") return 1 if args.json: - _print_json(asdict(result)) + _print_json(asdict(generate_result)) return 0 - stats = result.stats or { + stats = generate_result.stats or { "datasource_count": 0, "pipe_count": 0, "connection_count": 0, @@ -121,12 +137,12 @@ def main(argv: list[str] | None = None) -> int: f"{stats['pipe_count']} pipes, " f"{stats['connection_count']} connections)" ) - if result.output_dir: - print(f"Written to: {result.output_dir}") - print(f"Completed in {output.format_duration(result.duration_ms)}") + if generate_result.output_dir: + print(f"Written to: {generate_result.output_dir}") + print(f"Completed in {output.format_duration(generate_result.duration_ms)}") return 0 - result = run_migrate( + migrate_result = run_migrate( { "cwd": args.cwd, "patterns": args.patterns, @@ -138,17 +154,17 @@ def main(argv: list[str] | None = None) -> int: ) if args.json: - _print_json(result) - return 0 if result["success"] else 1 + _print_json(migrate_result) + return 0 if migrate_result["success"] else 1 - if result["success"]: - migrated_count = len(result.get("migrated") or []) + if migrate_result["success"]: + migrated_count = len(migrate_result.get("migrated") or []) print(f"Migrated {migrated_count} resources") - if result.get("output_path"): - print(f"Written to: {result['output_path']}") + if migrate_result.get("output_path"): + print(f"Written to: {migrate_result['output_path']}") return 0 - errors = result.get("errors") or [] + errors = migrate_result.get("errors") or [] if errors: output.error(f"Migrate failed with {len(errors)} error(s)") for error in errors: diff --git a/src/tinybird_sdk/cli/output.py b/src/tinybird_sdk/cli/output.py index ed8eee4..1475160 100644 --- a/src/tinybird_sdk/cli/output.py +++ b/src/tinybird_sdk/cli/output.py @@ -98,7 +98,9 @@ def show_build_errors(errors: list[dict[str, str]]) -> None: def show_build_success(duration_ms: int, is_rebuild: bool = False) -> None: - success(f"\n✓ {'Rebuild' if is_rebuild else 'Build'} completed in {format_duration(duration_ms)}") + success( + f"\n✓ {'Rebuild' if is_rebuild else 'Build'} completed in {format_duration(duration_ms)}" + ) def show_build_failure(is_rebuild: bool = False) -> None: diff --git a/src/tinybird_sdk/cli/region_selector.py b/src/tinybird_sdk/cli/region_selector.py index d591bf3..1268843 100644 --- a/src/tinybird_sdk/cli/region_selector.py +++ b/src/tinybird_sdk/cli/region_selector.py @@ -10,7 +10,9 @@ FALLBACK_REGIONS: list[TinybirdRegion] = [ TinybirdRegion(name="EU (GCP)", api_host="https://api.tinybird.co", provider="gcp"), - TinybirdRegion(name="US East (AWS)", api_host="https://api.us-east-1.aws.tinybird.co", provider="aws"), + TinybirdRegion( + name="US East (AWS)", api_host="https://api.us-east-1.aws.tinybird.co", provider="aws" + ), ] @@ -34,15 +36,28 @@ def select_region(default_api_host: str | None = None) -> RegionSelectionResult: regions = sorted(regions, key=lambda region: (region.provider != "gcp", region.name)) if default_api_host: - match = next((region for region in regions if region.api_host.rstrip("/") == default_api_host.rstrip("/")), None) + match = next( + ( + region + for region in regions + if region.api_host.rstrip("/") == default_api_host.rstrip("/") + ), + None, + ) if match: - return RegionSelectionResult(success=True, api_host=match.api_host, region_name=match.name) + return RegionSelectionResult( + success=True, api_host=match.api_host, region_name=match.name + ) env_region = os.getenv("TINYBIRD_REGION") if env_region: - match = next((region for region in regions if env_region in {region.name, region.api_host}), None) + match = next( + (region for region in regions if env_region in {region.name, region.api_host}), None + ) if match: - return RegionSelectionResult(success=True, api_host=match.api_host, region_name=match.name) + return RegionSelectionResult( + success=True, api_host=match.api_host, region_name=match.name + ) # Non-interactive default selection. chosen = regions[0] diff --git a/src/tinybird_sdk/cli/utils/schema_validation.py b/src/tinybird_sdk/cli/utils/schema_validation.py index 92785ee..616e600 100644 --- a/src/tinybird_sdk/cli/utils/schema_validation.py +++ b/src/tinybird_sdk/cli/utils/schema_validation.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any +from typing import Any, TypedDict from ...client.base import TinybirdClient from ...schema.pipe import PipeDefinition @@ -32,8 +32,21 @@ class SchemaValidationResult: pipes_skipped: list[str] -def validate_pipe_schemas(options: SchemaValidationOptions | dict[str, Any]) -> SchemaValidationResult: - normalized = options if isinstance(options, SchemaValidationOptions) else SchemaValidationOptions(**options) +class _SchemaValidationSummary(TypedDict): + valid: bool + missing_columns: list[dict[str, str]] + extra_columns: list[dict[str, str]] + type_mismatches: list[dict[str, str]] + + +def validate_pipe_schemas( + options: SchemaValidationOptions | dict[str, Any], +) -> SchemaValidationResult: + normalized = ( + options + if isinstance(options, SchemaValidationOptions) + else SchemaValidationOptions(**options) + ) client = TinybirdClient({"base_url": normalized.base_url, "token": normalized.token}) @@ -105,7 +118,9 @@ def validate_pipe_schemas(options: SchemaValidationOptions | dict[str, Any]) -> except Exception: skipped.append(pipe_name) - return SchemaValidationResult(valid=valid, issues=issues, pipes_validated=validated, pipes_skipped=skipped) + return SchemaValidationResult( + valid=valid, issues=issues, pipes_validated=validated, pipes_skipped=skipped + ) def _has_required_params(pipe: PipeDefinition) -> bool: @@ -142,8 +157,10 @@ def _types_are_compatible(actual: str, expected: str) -> bool: return _normalize_type(actual) == _normalize_type(expected) -def _validate_output_schema(response_meta: list[dict[str, str]], output_schema: dict[str, Any]) -> dict[str, Any]: - result = { +def _validate_output_schema( + response_meta: list[dict[str, str]], output_schema: dict[str, Any] +) -> _SchemaValidationSummary: + result: _SchemaValidationSummary = { "valid": True, "missing_columns": [], "extra_columns": [], diff --git a/src/tinybird_sdk/client/__init__.py b/src/tinybird_sdk/client/__init__.py index 0b08f16..fef7ec4 100644 --- a/src/tinybird_sdk/client/__init__.py +++ b/src/tinybird_sdk/client/__init__.py @@ -6,7 +6,12 @@ IngestResult, ClientConfig, ) -from .preview import is_preview_environment, get_preview_branch_name, resolve_token, clear_token_cache +from .preview import ( + is_preview_environment, + get_preview_branch_name, + resolve_token, + clear_token_cache, +) __all__ = [ "TinybirdClient", diff --git a/src/tinybird_sdk/client/base.py b/src/tinybird_sdk/client/base.py index 9be51d9..6f0fb36 100644 --- a/src/tinybird_sdk/client/base.py +++ b/src/tinybird_sdk/client/base.py @@ -1,21 +1,23 @@ from __future__ import annotations from dataclasses import asdict -from typing import Any +from typing import Any, cast from ..api.api import TinybirdApi, TinybirdApiError from ..api.branches import get_or_create_branch from ..cli.config import load_config_async from .preview import get_preview_branch_name, is_preview_environment from .tokens import TokensNamespace -from .types import ClientContext, TinybirdError +from .types import ClientContext, TinybirdError, TinybirdErrorResponse class _DatasourcesNamespace: def __init__(self, client: "TinybirdClient"): self._client = client - def ingest(self, datasource_name: str, event: dict[str, Any], options: dict[str, Any] | None = None) -> dict[str, Any]: + def ingest( + self, datasource_name: str, event: dict[str, Any], options: dict[str, Any] | None = None + ) -> dict[str, Any]: return self._client._ingest_datasource(datasource_name, event, options or {}) def append(self, datasource_name: str, options: dict[str, Any]) -> dict[str, Any]: @@ -27,7 +29,9 @@ def replace(self, datasource_name: str, options: dict[str, Any]) -> dict[str, An def delete(self, datasource_name: str, options: dict[str, Any]) -> dict[str, Any]: return self._client._delete_datasource(datasource_name, options) - def truncate(self, datasource_name: str, options: dict[str, Any] | None = None) -> dict[str, Any]: + def truncate( + self, datasource_name: str, options: dict[str, Any] | None = None + ) -> dict[str, Any]: return self._client._truncate_datasource(datasource_name, options or {}) @@ -55,13 +59,17 @@ def _append_datasource(self, datasource_name: str, options: dict[str, Any]) -> d return self._get_api(token).append_datasource(datasource_name, options) except Exception as error: self._rethrow_api_error(error) + raise AssertionError("unreachable") def _replace_datasource(self, datasource_name: str, options: dict[str, Any]) -> dict[str, Any]: token = self._get_token() try: - return self._get_api(token).append_datasource(datasource_name, options, {"mode": "replace"}) + return self._get_api(token).append_datasource( + datasource_name, options, {"mode": "replace"} + ) except Exception as error: self._rethrow_api_error(error) + raise AssertionError("unreachable") def _delete_datasource(self, datasource_name: str, options: dict[str, Any]) -> dict[str, Any]: token = self._get_token() @@ -69,6 +77,7 @@ def _delete_datasource(self, datasource_name: str, options: dict[str, Any]) -> d return self._get_api(token).delete_datasource(datasource_name, options) except Exception as error: self._rethrow_api_error(error) + raise AssertionError("unreachable") def _truncate_datasource(self, datasource_name: str, options: dict[str, Any]) -> dict[str, Any]: token = self._get_token() @@ -76,13 +85,17 @@ def _truncate_datasource(self, datasource_name: str, options: dict[str, Any]) -> return self._get_api(token).truncate_datasource(datasource_name, options) except Exception as error: self._rethrow_api_error(error) + raise AssertionError("unreachable") - def _ingest_datasource(self, datasource_name: str, event: dict[str, Any], options: dict[str, Any]) -> dict[str, Any]: + def _ingest_datasource( + self, datasource_name: str, event: dict[str, Any], options: dict[str, Any] + ) -> dict[str, Any]: token = self._get_token() try: return self._get_api(token).ingest_batch(datasource_name, [event], options) except Exception as error: self._rethrow_api_error(error) + raise AssertionError("unreachable") def _get_token(self) -> str: return self._resolve_context().token @@ -174,14 +187,22 @@ def _resolve_branch_context(self) -> ClientContext: except Exception as error: raise TinybirdError(f"Failed to resolve branch context: {error}", 500) from error - def query(self, pipe_name: str, params: dict[str, Any] | None = None, options: dict[str, Any] | None = None) -> dict[str, Any]: + def query( + self, + pipe_name: str, + params: dict[str, Any] | None = None, + options: dict[str, Any] | None = None, + ) -> dict[str, Any]: token = self._get_token() try: return self._get_api(token).query(pipe_name, params or {}, options or {}) except Exception as error: self._rethrow_api_error(error) + raise AssertionError("unreachable") - def ingest(self, datasource_name: str, event: dict[str, Any], options: dict[str, Any] | None = None) -> dict[str, Any]: + def ingest( + self, datasource_name: str, event: dict[str, Any], options: dict[str, Any] | None = None + ) -> dict[str, Any]: return self.datasources.ingest(datasource_name, event, options or {}) def ingest_batch( @@ -195,6 +216,7 @@ def ingest_batch( return self._get_api(token).ingest_batch(datasource_name, events, options or {}) except Exception as error: self._rethrow_api_error(error) + raise AssertionError("unreachable") def sql(self, sql: str, options: dict[str, Any] | None = None) -> dict[str, Any]: token = self._get_token() @@ -202,6 +224,7 @@ def sql(self, sql: str, options: dict[str, Any] | None = None) -> dict[str, Any] return self._get_api(token).sql(sql, options or {}) except Exception as error: self._rethrow_api_error(error) + raise AssertionError("unreachable") def get_context(self) -> dict[str, Any]: return asdict(self._resolve_context()) @@ -222,7 +245,8 @@ def _get_api(self, token: str) -> TinybirdApi: def _rethrow_api_error(self, error: Exception) -> None: if isinstance(error, TinybirdApiError): - raise TinybirdError(str(error), error.status_code, error.response) from error + response = cast(TinybirdErrorResponse | None, error.response) + raise TinybirdError(str(error), error.status_code, response) from error raise error diff --git a/src/tinybird_sdk/client/preview.py b/src/tinybird_sdk/client/preview.py index 2edfde0..1589255 100644 --- a/src/tinybird_sdk/client/preview.py +++ b/src/tinybird_sdk/client/preview.py @@ -89,7 +89,9 @@ def resolve_token(options: dict[str, Any] | None = None) -> str: if _cached_branch_token and _cached_branch_name == branch_name: return _cached_branch_token - base_url = options.get("base_url") or os.getenv("TINYBIRD_URL") or "https://api.tinybird.co" + base_url = ( + options.get("base_url") or os.getenv("TINYBIRD_URL") or "https://api.tinybird.co" + ) branch_token = _fetch_branch_token(base_url, configured_token, branch_name) if branch_token: _cached_branch_token = branch_token diff --git a/src/tinybird_sdk/codegen/index.py b/src/tinybird_sdk/codegen/index.py index bf42711..8b8cb42 100644 --- a/src/tinybird_sdk/codegen/index.py +++ b/src/tinybird_sdk/codegen/index.py @@ -13,7 +13,7 @@ def generate_datasource_code(ds: DatasourceInfo) -> str: lines: list[str] = [] if ds.description: - lines.extend(["\"\"\"", ds.description, "\"\"\""]) + lines.extend(['"""', ds.description, '"""']) lines.append(f"{var_name} = define_datasource({ds.name!r}, {{") if ds.description: @@ -52,7 +52,7 @@ def generate_pipe_code(pipe: PipeInfo) -> str: define_func = "define_copy_pipe" if pipe.description: - lines.extend(["\"\"\"", pipe.description, "\"\"\""]) + lines.extend(['"""', pipe.description, '"""']) lines.append(f"{var_name} = {define_func}({pipe.name!r}, {{") if pipe.description: @@ -127,7 +127,9 @@ def generate_pipes_file(pipes: list[PipeInfo], datasources: list[DatasourceInfo] referenced.add(pipe.copy["target_datasource"]) if referenced: - lines.append(f"from .datasources import {', '.join(to_snake_case(name) for name in sorted(referenced))}") + lines.append( + f"from .datasources import {', '.join(to_snake_case(name) for name in sorted(referenced))}" + ) lines.append("") if not pipes: @@ -144,11 +146,15 @@ def generate_client_file(datasources: list[DatasourceInfo], pipes: list[PipeInfo ] if datasources: - lines.append(f"from .datasources import {', '.join(to_snake_case(ds.name) for ds in datasources)}") + lines.append( + f"from .datasources import {', '.join(to_snake_case(ds.name) for ds in datasources)}" + ) endpoint_pipes = [pipe for pipe in pipes if pipe.type == "endpoint"] if endpoint_pipes: - lines.append(f"from .pipes import {', '.join(to_snake_case(pipe.name) for pipe in endpoint_pipes)}") + lines.append( + f"from .pipes import {', '.join(to_snake_case(pipe.name) for pipe in endpoint_pipes)}" + ) lines.extend( [ diff --git a/src/tinybird_sdk/codegen/type_mapper.py b/src/tinybird_sdk/codegen/type_mapper.py index 4e545a6..290f7e7 100644 --- a/src/tinybird_sdk/codegen/type_mapper.py +++ b/src/tinybird_sdk/codegen/type_mapper.py @@ -97,7 +97,9 @@ def clickhouse_type_to_validator(ch_type: str) -> str: agg = re.match(r"^AggregateFunction\((\w+),\s*(.+)\)$", ch_type) if agg: - return f't.aggregate_function("{agg.group(1)}", {clickhouse_type_to_validator(agg.group(2))})' + return ( + f't.aggregate_function("{agg.group(1)}", {clickhouse_type_to_validator(agg.group(2))})' + ) if ch_type.startswith("Nested("): return "t.json()" @@ -105,7 +107,9 @@ def clickhouse_type_to_validator(ch_type: str) -> str: return f"t.string() # TODO: Unknown type: {ch_type}" -def param_type_to_validator(param_type: str, default_value: str | int | None = None, required: bool = True) -> str: +def param_type_to_validator( + param_type: str, default_value: str | int | None = None, required: bool = True +) -> str: param_type = param_type.strip() mapping = { "String": "p.string()", diff --git a/src/tinybird_sdk/codegen/utils.py b/src/tinybird_sdk/codegen/utils.py index c57d966..bc197b9 100644 --- a/src/tinybird_sdk/codegen/utils.py +++ b/src/tinybird_sdk/codegen/utils.py @@ -122,7 +122,9 @@ def generate_engine_code(engine: dict[str, str | None]) -> str: if engine.get("type") == "ReplacingMergeTree" and engine.get("ver"): options.append(f"'ver': {engine['ver']!r}") - if engine.get("type") in {"CollapsingMergeTree", "VersionedCollapsingMergeTree"} and engine.get("sign"): + if engine.get("type") in {"CollapsingMergeTree", "VersionedCollapsingMergeTree"} and engine.get( + "sign" + ): options.append(f"'sign': {engine['sign']!r}") if engine.get("type") == "VersionedCollapsingMergeTree" and engine.get("version"): diff --git a/src/tinybird_sdk/generator/client.py b/src/tinybird_sdk/generator/client.py index ce52fb5..47da9c1 100644 --- a/src/tinybird_sdk/generator/client.py +++ b/src/tinybird_sdk/generator/client.py @@ -42,7 +42,9 @@ class GeneratedClient: def generate_client_file(options: GenerateClientOptions | dict[str, Any]) -> GeneratedClient: - normalized = options if isinstance(options, GenerateClientOptions) else GenerateClientOptions(**options) + normalized = ( + options if isinstance(options, GenerateClientOptions) else GenerateClientOptions(**options) + ) output = Path(normalized.output_path) absolute_path = output if output.is_absolute() else Path(normalized.cwd) / output diff --git a/src/tinybird_sdk/generator/connection.py b/src/tinybird_sdk/generator/connection.py index 8c52a6d..06e7483 100644 --- a/src/tinybird_sdk/generator/connection.py +++ b/src/tinybird_sdk/generator/connection.py @@ -72,13 +72,21 @@ def _generate_gcs_connection(connection: GCSConnectionDefinition) -> str: def generate_connection(connection: ConnectionDefinition) -> GeneratedConnection: if isinstance(connection, KafkaConnectionDefinition): - return GeneratedConnection(name=connection._name, content=_generate_kafka_connection(connection)) + return GeneratedConnection( + name=connection._name, content=_generate_kafka_connection(connection) + ) if isinstance(connection, S3ConnectionDefinition): - return GeneratedConnection(name=connection._name, content=_generate_s3_connection(connection)) + return GeneratedConnection( + name=connection._name, content=_generate_s3_connection(connection) + ) if isinstance(connection, GCSConnectionDefinition): - return GeneratedConnection(name=connection._name, content=_generate_gcs_connection(connection)) + return GeneratedConnection( + name=connection._name, content=_generate_gcs_connection(connection) + ) raise ValueError(f"Unsupported connection type: {connection._connectionType}") -def generate_all_connections(connections: dict[str, ConnectionDefinition]) -> list[GeneratedConnection]: +def generate_all_connections( + connections: dict[str, ConnectionDefinition], +) -> list[GeneratedConnection]: return [generate_connection(connection) for connection in connections.values()] diff --git a/src/tinybird_sdk/generator/datasource.py b/src/tinybird_sdk/generator/datasource.py index b3202aa..d3e7505 100644 --- a/src/tinybird_sdk/generator/datasource.py +++ b/src/tinybird_sdk/generator/datasource.py @@ -46,7 +46,9 @@ def _format_default_value(value: Any, tinybird_type: str) -> str: return f"'{_escape_sql_string(str(value))}'" -def _generate_column_line(column_name: str, column: TypeValidator | ColumnDefinition, include_json_paths: bool) -> str: +def _generate_column_line( + column_name: str, column: TypeValidator | ColumnDefinition, include_json_paths: bool +) -> str: validator = get_column_type(column) json_path = get_column_json_path(column) tinybird_type = validator._tinybirdType @@ -54,14 +56,18 @@ def _generate_column_line(column_name: str, column: TypeValidator | ColumnDefini parts = [f" {column_name} {tinybird_type}"] if include_json_paths: - effective_json_path = json_path if isinstance(json_path, str) and json_path else f"$.{column_name}" + effective_json_path = ( + json_path if isinstance(json_path, str) and json_path else f"$.{column_name}" + ) parts.append(f"`json:{effective_json_path}`") if validator._modifiers.has_default: if isinstance(validator._modifiers.default_expression, str): parts.append(f"DEFAULT {validator._modifiers.default_expression}") else: - parts.append(f"DEFAULT {_format_default_value(validator._modifiers.default_value, tinybird_type)}") + parts.append( + f"DEFAULT {_format_default_value(validator._modifiers.default_value, tinybird_type)}" + ) if validator._modifiers.codec: parts.append(f"CODEC({validator._modifiers.codec})") @@ -202,5 +208,7 @@ def generate_datasource(datasource: DatasourceDefinition) -> GeneratedDatasource return GeneratedDatasource(name=datasource._name, content="\n".join(parts)) -def generate_all_datasources(datasources: dict[str, DatasourceDefinition]) -> list[GeneratedDatasource]: +def generate_all_datasources( + datasources: dict[str, DatasourceDefinition], +) -> list[GeneratedDatasource]: return [generate_datasource(datasource) for datasource in datasources.values()] diff --git a/src/tinybird_sdk/generator/include_paths.py b/src/tinybird_sdk/generator/include_paths.py index 10a5850..10659b5 100644 --- a/src/tinybird_sdk/generator/include_paths.py +++ b/src/tinybird_sdk/generator/include_paths.py @@ -34,7 +34,9 @@ def resolve_include_files(include_paths: list[str], cwd: str) -> list[ResolvedIn for include_path in include_paths: if _has_glob(include_path): - absolute_pattern = include_path if Path(include_path).is_absolute() else str(base / include_path) + absolute_pattern = ( + include_path if Path(include_path).is_absolute() else str(base / include_path) + ) matches = sorted(glob.glob(absolute_pattern, recursive=True)) matches = [m for m in matches if Path(m).is_file() and not _is_ignored(Path(m))] if not matches: @@ -45,7 +47,11 @@ def resolve_include_files(include_paths: list[str], cwd: str) -> list[ResolvedIn if key in seen: continue seen.add(key) - source = match if Path(include_path).is_absolute() else _normalize(str(Path(match).resolve().relative_to(base))) + source = ( + match + if Path(include_path).is_absolute() + else _normalize(str(Path(match).resolve().relative_to(base))) + ) resolved.append(ResolvedIncludeFile(source_path=source, absolute_path=key)) continue @@ -84,7 +90,9 @@ def get_include_watch_directories(include_paths: list[str], cwd: str) -> list[st for include_path in include_paths: if _has_glob(include_path): - absolute_pattern = include_path if Path(include_path).is_absolute() else str(base / include_path) + absolute_pattern = ( + include_path if Path(include_path).is_absolute() else str(base / include_path) + ) pattern = Path(absolute_pattern) anchor_parts: list[str] = [] for part in pattern.parts: diff --git a/src/tinybird_sdk/generator/index.py b/src/tinybird_sdk/generator/index.py index 580482f..4d4969f 100644 --- a/src/tinybird_sdk/generator/index.py +++ b/src/tinybird_sdk/generator/index.py @@ -3,7 +3,12 @@ from dataclasses import dataclass from typing import Any -from ..schema.project import ConnectionsDefinition, DatasourcesDefinition, PipesDefinition, ProjectDefinition +from ..schema.project import ( + ConnectionsDefinition, + DatasourcesDefinition, + PipesDefinition, + ProjectDefinition, +) from .client import GenerateClientOptions, GeneratedClient, generate_client_file from .connection import GeneratedConnection, generate_all_connections from .datasource import GeneratedDatasource, generate_all_datasources @@ -95,7 +100,11 @@ def generate_resources_from_entities( def build_from_include(options: BuildFromIncludeOptions | dict[str, Any]) -> BuildFromIncludeResult: - normalized = options if isinstance(options, BuildFromIncludeOptions) else BuildFromIncludeOptions(**options) + normalized = ( + options + if isinstance(options, BuildFromIncludeOptions) + else BuildFromIncludeOptions(**options) + ) entities = load_entities( LoadEntitiesOptions(include_paths=normalized.include_paths, cwd=normalized.cwd) diff --git a/src/tinybird_sdk/generator/loader.py b/src/tinybird_sdk/generator/loader.py index fdb1995..fc882a2 100644 --- a/src/tinybird_sdk/generator/loader.py +++ b/src/tinybird_sdk/generator/loader.py @@ -116,15 +116,23 @@ def load_schema(options: LoaderOptions | dict[str, Any]) -> LoadedSchema: f"No ProjectDefinition found in {schema_path}. Export `project` or a value created with define_project()." ) - return LoadedSchema(project=project, schema_path=str(schema_path), schema_dir=str(schema_path.parent)) + return LoadedSchema( + project=project, schema_path=str(schema_path), schema_dir=str(schema_path.parent) + ) def _is_raw_datafile(source_path: str) -> bool: - return source_path.endswith(".datasource") or source_path.endswith(".pipe") or source_path.endswith(".connection") + return ( + source_path.endswith(".datasource") + or source_path.endswith(".pipe") + or source_path.endswith(".connection") + ) def load_entities(options: LoadEntitiesOptions | dict[str, Any]) -> LoadedEntities: - normalized = options if isinstance(options, LoadEntitiesOptions) else LoadEntitiesOptions(**options) + normalized = ( + options if isinstance(options, LoadEntitiesOptions) else LoadEntitiesOptions(**options) + ) cwd = Path(normalized.cwd or ".").resolve() include_files = resolve_include_files(normalized.include_paths, str(cwd)) @@ -194,11 +202,15 @@ def entities_to_project(entities: LoadedEntities) -> dict[str, Any]: } -def watch_schema(options: WatchOptions | dict[str, Any], callback: Callable[[], None]) -> WatchController: +def watch_schema( + options: WatchOptions | dict[str, Any], callback: Callable[[], None] +) -> WatchController: normalized = options if isinstance(options, WatchOptions) else WatchOptions(**options) cwd = Path(normalized.cwd or ".").resolve() interval = max(normalized.interval_ms, 100) / 1000.0 - watch_dirs = [Path(p) for p in get_include_watch_directories(normalized.include_paths, str(cwd))] + watch_dirs = [ + Path(p) for p in get_include_watch_directories(normalized.include_paths, str(cwd)) + ] stop_flag = {"stop": False} mtimes: dict[str, float] = {} diff --git a/src/tinybird_sdk/migrate/emit_ts.py b/src/tinybird_sdk/migrate/emit_ts.py index f9b304c..55a2f67 100644 --- a/src/tinybird_sdk/migrate/emit_ts.py +++ b/src/tinybird_sdk/migrate/emit_ts.py @@ -81,7 +81,9 @@ def _strict_param_base_validator(type_name: str) -> str: return validator -def _apply_param_optional(base_validator: str, required: bool, default_value: str | int | float | bool | None) -> str: +def _apply_param_optional( + base_validator: str, required: bool, default_value: str | int | float | bool | None +) -> str: if required and default_value is None: return base_validator @@ -118,7 +120,9 @@ def _emit_engine_options(engine_model: Any) -> str: if len(engine.sorting_key) == 1: options.append(f"'sorting_key': {_escape_string(engine.sorting_key[0])}") else: - options.append(f"'sorting_key': [{', '.join(_escape_string(v) for v in engine.sorting_key)}]") + options.append( + f"'sorting_key': [{', '.join(_escape_string(v) for v in engine.sorting_key)}]" + ) if engine.partition_key: options.append(f"'partition_key': {_escape_string(engine.partition_key)}") @@ -126,7 +130,9 @@ def _emit_engine_options(engine_model: Any) -> str: if len(engine.primary_key) == 1: options.append(f"'primary_key': {_escape_string(engine.primary_key[0])}") else: - options.append(f"'primary_key': [{', '.join(_escape_string(v) for v in engine.primary_key)}]") + options.append( + f"'primary_key': [{', '.join(_escape_string(v) for v in engine.primary_key)}]" + ) if engine.ttl: options.append(f"'ttl': {_escape_string(engine.ttl)}") if engine.ver: @@ -138,7 +144,9 @@ def _emit_engine_options(engine_model: Any) -> str: if engine.version: options.append(f"'version': {_escape_string(engine.version)}") if engine.summing_columns: - options.append(f"'columns': [{', '.join(_escape_string(v) for v in engine.summing_columns)}]") + options.append( + f"'columns': [{', '.join(_escape_string(v) for v in engine.summing_columns)}]" + ) if engine.settings: settings_entries = [] for key, value in engine.settings.items(): @@ -224,7 +232,9 @@ def _emit_datasource(ds: DatasourceModel) -> str: if ds.kafka.group_id: lines.append(f" 'group_id': {_escape_string(ds.kafka.group_id)},") if ds.kafka.auto_offset_reset: - lines.append(f" 'auto_offset_reset': {_escape_string(ds.kafka.auto_offset_reset)},") + lines.append( + f" 'auto_offset_reset': {_escape_string(ds.kafka.auto_offset_reset)}," + ) if ds.kafka.store_raw_value is not None: lines.append(f" 'store_raw_value': {ds.kafka.store_raw_value},") lines.append(" },") @@ -265,7 +275,9 @@ def _emit_datasource(ds: DatasourceModel) -> str: lines.append(" ],") if ds.shared_with: - lines.append(f" 'shared_with': [{', '.join(_escape_string(v) for v in ds.shared_with)}],") + lines.append( + f" 'shared_with': [{', '.join(_escape_string(v) for v in ds.shared_with)}]," + ) lines.append("})") lines.append("") @@ -286,7 +298,9 @@ def _emit_kafka_connection(connection: KafkaConnectionModel) -> str: if connection.secret: lines.append(f" 'secret': {_escape_string(connection.secret)},") if connection.schema_registry_url: - lines.append(f" 'schema_registry_url': {_escape_string(connection.schema_registry_url)},") + lines.append( + f" 'schema_registry_url': {_escape_string(connection.schema_registry_url)}," + ) if connection.ssl_ca_pem: lines.append(f" 'ssl_ca_pem': {_escape_string(connection.ssl_ca_pem)},") lines.append("})") @@ -314,13 +328,17 @@ def _emit_gcs_connection(connection: GCSConnectionModel) -> str: variable_name = to_snake_case(connection.name) lines: list[str] = [] lines.append(f"{variable_name} = define_gcs_connection({_escape_string(connection.name)}, {{") - lines.append(f" 'service_account_credentials_json': {_escape_string(connection.service_account_credentials_json)},") + lines.append( + f" 'service_account_credentials_json': {_escape_string(connection.service_account_credentials_json)}," + ) lines.append("})") lines.append("") return "\n".join(lines) -def _emit_connection(connection: KafkaConnectionModel | S3ConnectionModel | GCSConnectionModel) -> str: +def _emit_connection( + connection: KafkaConnectionModel | S3ConnectionModel | GCSConnectionModel, +) -> str: if isinstance(connection, S3ConnectionModel): return _emit_s3_connection(connection) if isinstance(connection, GCSConnectionModel): @@ -403,7 +421,9 @@ def _emit_pipe(pipe: PipeModel) -> str: if pipe.type == "endpoint": if pipe.cache_ttl is not None: - lines.append(f" 'endpoint': {{'enabled': True, 'cache': {{'enabled': True, 'ttl': {pipe.cache_ttl}}}}},") + lines.append( + f" 'endpoint': {{'enabled': True, 'cache': {{'enabled': True, 'ttl': {pipe.cache_ttl}}}}}," + ) else: lines.append(" 'endpoint': True,") lines.append(" 'output': {") diff --git a/src/tinybird_sdk/migrate/parse_connection.py b/src/tinybird_sdk/migrate/parse_connection.py index fcc7785..0a932b1 100644 --- a/src/tinybird_sdk/migrate/parse_connection.py +++ b/src/tinybird_sdk/migrate/parse_connection.py @@ -1,7 +1,20 @@ from __future__ import annotations -from .parser_utils import MigrationParseError, is_blank, parse_directive_line, parse_quoted_value, read_directive_block, split_lines -from .types import ConnectionModel, GCSConnectionModel, KafkaConnectionModel, ResourceFile, S3ConnectionModel +from .parser_utils import ( + MigrationParseError, + is_blank, + parse_directive_line, + parse_quoted_value, + read_directive_block, + split_lines, +) +from .types import ( + ConnectionModel, + GCSConnectionModel, + KafkaConnectionModel, + ResourceFile, + S3ConnectionModel, +) CONNECTION_DIRECTIVES = { "TYPE", @@ -117,7 +130,9 @@ def parse_connection_file(resource: ResourceFile) -> ConnectionModel: i += 1 if not connection_type: - raise MigrationParseError(resource.file_path, "connection", resource.name, "TYPE directive is required.") + raise MigrationParseError( + resource.file_path, "connection", resource.name, "TYPE directive is required." + ) if connection_type == "kafka": if region or arn or access_key or access_secret or service_account_credentials_json: diff --git a/src/tinybird_sdk/migrate/parse_datasource.py b/src/tinybird_sdk/migrate/parse_datasource.py index 905e713..41ee768 100644 --- a/src/tinybird_sdk/migrate/parse_datasource.py +++ b/src/tinybird_sdk/migrate/parse_datasource.py @@ -81,7 +81,9 @@ def _find_token_outside_contexts(input_value: str, token: str) -> int: def _normalize_column_name(value: str) -> str: trimmed = value.strip() - if (trimmed.startswith("`") and trimmed.endswith("`")) or (trimmed.startswith('"') and trimmed.endswith('"')): + if (trimmed.startswith("`") and trimmed.endswith("`")) or ( + trimmed.startswith('"') and trimmed.endswith('"') + ): return trimmed[1:-1] return trimmed @@ -198,7 +200,11 @@ def _parse_token(file_path: str, resource_name: str, value: str) -> DatasourceTo ) raw_name, scope = parts - name = raw_name[1:-1] if raw_name.startswith('"') and raw_name.endswith('"') and len(raw_name) >= 2 else raw_name + name = ( + raw_name[1:-1] + if raw_name.startswith('"') and raw_name.endswith('"') and len(raw_name) >= 2 + else raw_name + ) if scope not in {"READ", "APPEND"}: raise MigrationParseError( file_path, @@ -215,7 +221,9 @@ def _parse_index_line(file_path: str, resource_name: str, raw_line: str) -> Data if not line: raise MigrationParseError(file_path, "datasource", resource_name, "Empty INDEXES line.") - match = re.fullmatch(r"(\S+)\s+(.+?)\s+TYPE\s+(.+?)\s+GRANULARITY\s+(\d+)", line, flags=re.IGNORECASE) + match = re.fullmatch( + r"(\S+)\s+(.+?)\s+TYPE\s+(.+?)\s+GRANULARITY\s+(\d+)", line, flags=re.IGNORECASE + ) if not match: raise MigrationParseError( file_path, @@ -286,7 +294,9 @@ def parse_datasource_file(resource: ResourceFile) -> DatasourceModel: if line == "DESCRIPTION >": block, next_index = _read_indented_block(lines, i + 1) if not block: - raise MigrationParseError(resource.file_path, "datasource", resource.name, "DESCRIPTION block is empty.") + raise MigrationParseError( + resource.file_path, "datasource", resource.name, "DESCRIPTION block is empty." + ) description = "\n".join(block) i = next_index continue @@ -294,7 +304,9 @@ def parse_datasource_file(resource: ResourceFile) -> DatasourceModel: if line == "SCHEMA >": block, next_index = _read_indented_block(lines, i + 1) if not block: - raise MigrationParseError(resource.file_path, "datasource", resource.name, "SCHEMA block is empty.") + raise MigrationParseError( + resource.file_path, "datasource", resource.name, "SCHEMA block is empty." + ) for schema_line in block: if is_blank(schema_line) or schema_line.strip().startswith("#"): continue @@ -305,7 +317,9 @@ def parse_datasource_file(resource: ResourceFile) -> DatasourceModel: if line == "INDEXES >": block, next_index = _read_indented_block(lines, i + 1) if not block: - raise MigrationParseError(resource.file_path, "datasource", resource.name, "INDEXES block is empty.") + raise MigrationParseError( + resource.file_path, "datasource", resource.name, "INDEXES block is empty." + ) for index_line in block: if is_blank(index_line) or index_line.strip().startswith("#"): continue @@ -316,7 +330,9 @@ def parse_datasource_file(resource: ResourceFile) -> DatasourceModel: if line == "FORWARD_QUERY >": block, next_index = _read_indented_block(lines, i + 1) if not block: - raise MigrationParseError(resource.file_path, "datasource", resource.name, "FORWARD_QUERY block is empty.") + raise MigrationParseError( + resource.file_path, "datasource", resource.name, "FORWARD_QUERY block is empty." + ) forward_query = "\n".join(block) i = next_index continue @@ -358,7 +374,9 @@ def parse_datasource_file(resource: ResourceFile) -> DatasourceModel: try: settings = _parse_engine_settings(value) except Exception as error: - raise MigrationParseError(resource.file_path, "datasource", resource.name, str(error)) from error + raise MigrationParseError( + resource.file_path, "datasource", resource.name, str(error) + ) from error elif key == "KAFKA_CONNECTION_NAME": kafka_connection_name = value.strip() elif key == "KAFKA_TOPIC": @@ -418,7 +436,9 @@ def parse_datasource_file(resource: ResourceFile) -> DatasourceModel: i += 1 if not columns: - raise MigrationParseError(resource.file_path, "datasource", resource.name, "SCHEMA block is required.") + raise MigrationParseError( + resource.file_path, "datasource", resource.name, "SCHEMA block is required." + ) has_engine_directives = ( len(sorting_key) > 0 @@ -436,7 +456,12 @@ def parse_datasource_file(resource: ResourceFile) -> DatasourceModel: engine_type = "MergeTree" if engine_type and not sorting_key: - raise MigrationParseError(resource.file_path, "datasource", resource.name, "ENGINE_SORTING_KEY directive is required.") + raise MigrationParseError( + resource.file_path, + "datasource", + resource.name, + "ENGINE_SORTING_KEY directive is required.", + ) kafka: DatasourceKafkaModel | None = None if ( diff --git a/src/tinybird_sdk/migrate/parse_pipe.py b/src/tinybird_sdk/migrate/parse_pipe.py index 3f7d534..52e9201 100644 --- a/src/tinybird_sdk/migrate/parse_pipe.py +++ b/src/tinybird_sdk/migrate/parse_pipe.py @@ -12,7 +12,17 @@ split_lines, split_top_level_comma, ) -from .types import PipeModel, PipeNodeModel, PipeParamModel, PipeTokenModel, ResourceFile, SinkKafkaModel, SinkModel, SinkS3Model +from .types import ( + PipeModel, + PipeNodeModel, + PipeParamModel, + PipeTokenModel, + PipeTypeModel, + ResourceFile, + SinkKafkaModel, + SinkModel, + SinkS3Model, +) PIPE_DIRECTIVES = { @@ -54,7 +64,9 @@ def _infer_output_columns_from_sql(sql: str) -> list[str]: columns: list[str] = [] for expression in expressions: - alias = re.search(r"\s+AS\s+`?([a-zA-Z_][a-zA-Z0-9_]*)`?\s*$", expression, flags=re.IGNORECASE) + alias = re.search( + r"\s+AS\s+`?([a-zA-Z_][a-zA-Z0-9_]*)`?\s*$", expression, flags=re.IGNORECASE + ) if alias: columns.append(alias.group(1)) continue @@ -143,7 +155,9 @@ def _parse_required_flag(raw_value: str) -> bool: raise ValueError(f'Unsupported required value: "{raw_value}"') -def _parse_param_options(raw_args: list[str]) -> tuple[str | int | float | bool | None, bool | None, str | None]: +def _parse_param_options( + raw_args: list[str], +) -> tuple[str | int | float | bool | None, bool | None, str | None]: default_value: str | int | float | bool | None = None required: bool | None = None description: str | None = None @@ -213,7 +227,11 @@ def _mask_parentheses_inside_quotes(value: str) -> str: full_call = expression[start : start + len(match.group(0))] open_paren = full_call.find("(") close_paren = full_call.rfind(")") - args_raw = full_call[open_paren + 1 : close_paren] if open_paren >= 0 and close_paren > open_paren else "" + args_raw = ( + full_call[open_paren + 1 : close_paren] + if open_paren >= 0 and close_paren > open_paren + else "" + ) calls.append( { @@ -332,7 +350,9 @@ def _infer_params_from_sql(sql: str, file_path: str, resource_name: str) -> list try: default_value, required, description = _parse_param_options(args[1:]) except Exception as error: - raise MigrationParseError(file_path, "pipe", resource_name, str(error)) from error + raise MigrationParseError( + file_path, "pipe", resource_name, str(error) + ) from error existing = params.get(param_name) if existing: @@ -417,7 +437,7 @@ def parse_pipe_file(resource: ResourceFile) -> PipeModel: raw_node_sqls: list[str] = [] tokens: list[PipeTokenModel] = [] description: str | None = None - pipe_type: PipeModel.__annotations__["type"] = "pipe" # type: ignore[assignment] + pipe_type: PipeTypeModel = "pipe" cache_ttl: int | None = None materialized_datasource: str | None = None deployment_method: str | None = None @@ -447,7 +467,9 @@ def parse_pipe_file(resource: ResourceFile) -> PipeModel: description = "\n".join(block) elif nodes: last = nodes[-1] - nodes[-1] = PipeNodeModel(name=last.name, sql=last.sql, description="\n".join(block)) + nodes[-1] = PipeNodeModel( + name=last.name, sql=last.sql, description="\n".join(block) + ) else: raise MigrationParseError( resource.file_path, @@ -461,7 +483,9 @@ def parse_pipe_file(resource: ResourceFile) -> PipeModel: if line.startswith("NODE "): node_name = line[len("NODE ") :].strip() if not node_name: - raise MigrationParseError(resource.file_path, "pipe", resource.name, "NODE directive requires a name.") + raise MigrationParseError( + resource.file_path, "pipe", resource.name, "NODE directive requires a name." + ) i += 1 i = _next_non_blank(lines, i) @@ -496,11 +520,17 @@ def parse_pipe_file(resource: ResourceFile) -> PipeModel: resource.file_path, "pipe", resource.name, - f'Node "{node_name}" has SQL marker '%' but no SQL body.', + f'Node "{node_name}" has SQL marker ' % " but no SQL body.", ) raw_node_sqls.append(sql) - nodes.append(PipeNodeModel(name=node_name, description=node_description, sql=_normalize_sql_placeholders(sql))) + nodes.append( + PipeNodeModel( + name=node_name, + description=node_description, + sql=_normalize_sql_placeholders(sql), + ) + ) i = next_index continue @@ -633,10 +663,14 @@ def parse_pipe_file(resource: ResourceFile) -> PipeModel: i += 1 if not nodes: - raise MigrationParseError(resource.file_path, "pipe", resource.name, "At least one NODE is required.") + raise MigrationParseError( + resource.file_path, "pipe", resource.name, "At least one NODE is required." + ) if pipe_type != "endpoint" and cache_ttl is not None: - raise MigrationParseError(resource.file_path, "pipe", resource.name, "CACHE is only supported for TYPE endpoint.") + raise MigrationParseError( + resource.file_path, "pipe", resource.name, "CACHE is only supported for TYPE endpoint." + ) if pipe_type == "materialized" and not materialized_datasource: raise MigrationParseError( @@ -679,11 +713,22 @@ def parse_pipe_file(resource: ResourceFile) -> PipeModel: sink: SinkModel | None = None if pipe_type == "sink": if not export_connection_name: - raise MigrationParseError(resource.file_path, "pipe", resource.name, "EXPORT_CONNECTION_NAME is required for TYPE sink.") + raise MigrationParseError( + resource.file_path, + "pipe", + resource.name, + "EXPORT_CONNECTION_NAME is required for TYPE sink.", + ) has_kafka_directives = export_topic is not None has_s3_directives = any( - value is not None for value in (export_bucket_uri, export_file_template, export_format, export_compression) + value is not None + for value in ( + export_bucket_uri, + export_file_template, + export_format, + export_compression, + ) ) if has_kafka_directives and has_s3_directives: @@ -694,7 +739,9 @@ def parse_pipe_file(resource: ResourceFile) -> PipeModel: "Sink pipe cannot mix Kafka and S3 export directives.", ) - inferred_service = export_service or ("kafka" if has_kafka_directives else "s3" if has_s3_directives else None) + inferred_service = export_service or ( + "kafka" if has_kafka_directives else "s3" if has_s3_directives else None + ) if not inferred_service: raise MigrationParseError( resource.file_path, @@ -754,7 +801,12 @@ def parse_pipe_file(resource: ResourceFile) -> PipeModel: resource.name, "Kafka export directives are not valid for S3 sinks.", ) - if not export_bucket_uri or not export_file_template or not export_format or not export_schedule: + if ( + not export_bucket_uri + or not export_file_template + or not export_format + or not export_schedule + ): raise MigrationParseError( resource.file_path, "pipe", diff --git a/src/tinybird_sdk/migrate/parser_utils.py b/src/tinybird_sdk/migrate/parser_utils.py index 6ab461b..fa33e7f 100644 --- a/src/tinybird_sdk/migrate/parser_utils.py +++ b/src/tinybird_sdk/migrate/parser_utils.py @@ -7,7 +7,9 @@ class MigrationParseError(Exception): - def __init__(self, file_path: str, resource_kind: ResourceKind, resource_name: str, message: str): + def __init__( + self, file_path: str, resource_kind: ResourceKind, resource_name: str, message: str + ): super().__init__(message) self.file_path = file_path self.resource_kind = resource_kind @@ -69,7 +71,9 @@ def parse_quoted_value(input: str) -> str: return trimmed -def parse_literal_from_datafile(value: str) -> str | int | float | bool | None | dict[str, Any] | list[Any]: +def parse_literal_from_datafile( + value: str, +) -> str | int | float | bool | None | dict[str, Any] | list[Any]: trimmed = value.strip() if trimmed == "NULL": diff --git a/src/tinybird_sdk/migrate/runner.py b/src/tinybird_sdk/migrate/runner.py index 50b241e..5783997 100644 --- a/src/tinybird_sdk/migrate/runner.py +++ b/src/tinybird_sdk/migrate/runner.py @@ -2,13 +2,20 @@ from dataclasses import dataclass, replace from pathlib import Path -from typing import Any +from typing import Any, cast from .discovery import discover_resource_files from .emit_ts import emit_migration_file_content, validate_resource_for_emission from .parse import parse_resource_file from .parser_utils import MigrationParseError -from .types import MigrationError, MigrationResult, ParsedResource, ResourceFile +from .types import ( + DatasourceGCSModel, + DatasourceS3Model, + MigrationError, + MigrationResult, + ParsedResource, + ResourceFile, +) @dataclass(frozen=True, slots=True) @@ -86,7 +93,9 @@ def run_migrate(options: MigrateOptions | dict[str, Any]) -> MigrationResult: migrated: list[ParsedResource] = [] migrated_connection_names: set[str] = set() migrated_datasource_names: set[str] = set() - parsed_connection_type_by_name = {connection.name: connection.connection_type for connection in parsed_connections} + parsed_connection_type_by_name = { + connection.name: connection.connection_type for connection in parsed_connections + } for connection in parsed_connections: try: @@ -114,14 +123,17 @@ def run_migrate(options: MigrateOptions | dict[str, Any]) -> MigrationResult: else None ) - if referenced_connection_name and referenced_connection_name not in migrated_connection_names: + if ( + referenced_connection_name + and referenced_connection_name not in migrated_connection_names + ): errors.append( MigrationError( file_path=datasource.file_path, resource_name=datasource.name, resource_kind=datasource.kind, message=( - f'Datasource references missing/unmigrated connection ' + f"Datasource references missing/unmigrated connection " f'"{referenced_connection_name}".' ), ) @@ -129,7 +141,9 @@ def run_migrate(options: MigrateOptions | dict[str, Any]) -> MigrationResult: continue if datasource.kafka: - kafka_connection_type = parsed_connection_type_by_name.get(datasource.kafka.connection_name) + kafka_connection_type = parsed_connection_type_by_name.get( + datasource.kafka.connection_name + ) if kafka_connection_type != "kafka": errors.append( MigrationError( @@ -137,7 +151,7 @@ def run_migrate(options: MigrateOptions | dict[str, Any]) -> MigrationResult: resource_name=datasource.name, resource_kind=datasource.kind, message=( - f'Datasource kafka ingestion requires a kafka connection, found ' + f"Datasource kafka ingestion requires a kafka connection, found " f'"{kafka_connection_type or "(none)"}".' ), ) @@ -147,7 +161,9 @@ def run_migrate(options: MigrateOptions | dict[str, Any]) -> MigrationResult: import_config = datasource.s3 or datasource.gcs normalized_datasource = datasource if import_config: - import_connection_type = parsed_connection_type_by_name.get(import_config.connection_name) + import_connection_type = parsed_connection_type_by_name.get( + import_config.connection_name + ) if import_connection_type not in {"s3", "gcs"}: errors.append( MigrationError( @@ -165,13 +181,13 @@ def run_migrate(options: MigrateOptions | dict[str, Any]) -> MigrationResult: if import_connection_type == "gcs": normalized_datasource = replace( datasource, - gcs=replace(import_config), + gcs=replace(cast(DatasourceGCSModel, import_config)), s3=None, ) else: normalized_datasource = replace( datasource, - s3=replace(import_config), + s3=replace(cast(DatasourceS3Model, import_config)), gcs=None, ) @@ -199,7 +215,7 @@ def run_migrate(options: MigrateOptions | dict[str, Any]) -> MigrationResult: resource_name=pipe.name, resource_kind=pipe.kind, message=( - f'Sink pipe references missing/unmigrated connection ' + f"Sink pipe references missing/unmigrated connection " f'"{sink_connection_name or "(none)"}".' ), ) @@ -233,7 +249,8 @@ def run_migrate(options: MigrateOptions | dict[str, Any]) -> MigrationResult: continue if pipe.type == "materialized" and ( - not pipe.materialized_datasource or pipe.materialized_datasource not in migrated_datasource_names + not pipe.materialized_datasource + or pipe.materialized_datasource not in migrated_datasource_names ): errors.append( MigrationError( @@ -241,7 +258,7 @@ def run_migrate(options: MigrateOptions | dict[str, Any]) -> MigrationResult: resource_name=pipe.name, resource_kind=pipe.kind, message=( - f'Materialized pipe references missing/unmigrated datasource ' + f"Materialized pipe references missing/unmigrated datasource " f'"{pipe.materialized_datasource or "(none)"}".' ), ) @@ -249,7 +266,8 @@ def run_migrate(options: MigrateOptions | dict[str, Any]) -> MigrationResult: continue if pipe.type == "copy" and ( - not pipe.copy_target_datasource or pipe.copy_target_datasource not in migrated_datasource_names + not pipe.copy_target_datasource + or pipe.copy_target_datasource not in migrated_datasource_names ): errors.append( MigrationError( @@ -257,7 +275,7 @@ def run_migrate(options: MigrateOptions | dict[str, Any]) -> MigrationResult: resource_name=pipe.name, resource_kind=pipe.kind, message=( - f'Copy pipe references missing/unmigrated datasource ' + f"Copy pipe references missing/unmigrated datasource " f'"{pipe.copy_target_datasource or "(none)"}".' ), ) diff --git a/src/tinybird_sdk/migrate/types.py b/src/tinybird_sdk/migrate/types.py index 1e64191..3ebbfd1 100644 --- a/src/tinybird_sdk/migrate/types.py +++ b/src/tinybird_sdk/migrate/types.py @@ -211,7 +211,9 @@ class GCSConnectionModel: ConnectionModel = KafkaConnectionModel | S3ConnectionModel | GCSConnectionModel -ParsedResource = DatasourceModel | PipeModel | KafkaConnectionModel | S3ConnectionModel | GCSConnectionModel +ParsedResource = ( + DatasourceModel | PipeModel | KafkaConnectionModel | S3ConnectionModel | GCSConnectionModel +) @dataclass(frozen=True, slots=True) diff --git a/src/tinybird_sdk/py.typed b/src/tinybird_sdk/py.typed index e69de29..8b13789 100644 --- a/src/tinybird_sdk/py.typed +++ b/src/tinybird_sdk/py.typed @@ -0,0 +1 @@ + diff --git a/src/tinybird_sdk/schema/__init__.py b/src/tinybird_sdk/schema/__init__.py index d60a038..c3bf614 100644 --- a/src/tinybird_sdk/schema/__init__.py +++ b/src/tinybird_sdk/schema/__init__.py @@ -1,5 +1,20 @@ -from .types import t, TypeValidator, TypeModifiers, is_type_validator, get_tinybird_type, get_modifiers -from .params import p, ParamValidator, is_param_validator, get_param_tinybird_type, is_param_required, get_param_default, get_param_description +from .types import ( + t, + TypeValidator, + TypeModifiers, + is_type_validator, + get_tinybird_type, + get_modifiers, +) +from .params import ( + p, + ParamValidator, + is_param_validator, + get_param_tinybird_type, + is_param_required, + get_param_default, + get_param_description, +) from .engines import engine, EngineConfig, get_engine_clause, get_sorting_key, get_primary_key from .datasource import ( define_datasource, diff --git a/src/tinybird_sdk/schema/connection.py b/src/tinybird_sdk/schema/connection.py index f862bdd..9ab8708 100644 --- a/src/tinybird_sdk/schema/connection.py +++ b/src/tinybird_sdk/schema/connection.py @@ -68,28 +68,44 @@ class GCSConnectionDefinition: ConnectionDefinition = KafkaConnectionDefinition | S3ConnectionDefinition | GCSConnectionDefinition -def define_kafka_connection(name: str, options: dict[str, Any] | KafkaConnectionOptions) -> KafkaConnectionDefinition: +def define_kafka_connection( + name: str, options: dict[str, Any] | KafkaConnectionOptions +) -> KafkaConnectionDefinition: _validate_connection_name(name) - normalized = options if isinstance(options, KafkaConnectionOptions) else KafkaConnectionOptions(**options) + normalized = ( + options + if isinstance(options, KafkaConnectionOptions) + else KafkaConnectionOptions(**options) + ) return KafkaConnectionDefinition(_name=name, options=normalized) -def define_s3_connection(name: str, options: dict[str, Any] | S3ConnectionOptions) -> S3ConnectionDefinition: +def define_s3_connection( + name: str, options: dict[str, Any] | S3ConnectionOptions +) -> S3ConnectionDefinition: _validate_connection_name(name) - normalized = options if isinstance(options, S3ConnectionOptions) else S3ConnectionOptions(**options) + normalized = ( + options if isinstance(options, S3ConnectionOptions) else S3ConnectionOptions(**options) + ) if not normalized.arn and not (normalized.access_key and normalized.secret): raise ValueError("S3 connection requires either `arn` or both `access_key` and `secret`.") - if (normalized.access_key and not normalized.secret) or (not normalized.access_key and normalized.secret): + if (normalized.access_key and not normalized.secret) or ( + not normalized.access_key and normalized.secret + ): raise ValueError("S3 connection `access_key` and `secret` must be provided together.") return S3ConnectionDefinition(_name=name, options=normalized) -def define_gcs_connection(name: str, options: dict[str, Any] | GCSConnectionOptions) -> GCSConnectionDefinition: +def define_gcs_connection( + name: str, options: dict[str, Any] | GCSConnectionOptions +) -> GCSConnectionDefinition: _validate_connection_name(name) - normalized = options if isinstance(options, GCSConnectionOptions) else GCSConnectionOptions(**options) + normalized = ( + options if isinstance(options, GCSConnectionOptions) else GCSConnectionOptions(**options) + ) if not normalized.service_account_credentials_json.strip(): raise ValueError("GCS connection `service_account_credentials_json` is required.") @@ -98,7 +114,9 @@ def define_gcs_connection(name: str, options: dict[str, Any] | GCSConnectionOpti def is_connection_definition(value: Any) -> bool: - return isinstance(value, (KafkaConnectionDefinition, S3ConnectionDefinition, GCSConnectionDefinition)) + return isinstance( + value, (KafkaConnectionDefinition, S3ConnectionDefinition, GCSConnectionDefinition) + ) def is_kafka_connection_definition(value: Any) -> bool: diff --git a/src/tinybird_sdk/schema/datasource.py b/src/tinybird_sdk/schema/datasource.py index a372471..eb951d6 100644 --- a/src/tinybird_sdk/schema/datasource.py +++ b/src/tinybird_sdk/schema/datasource.py @@ -97,7 +97,9 @@ def _schema(self) -> SchemaDefinition: return self.options.schema -def define_datasource(name: str, options: dict[str, Any] | DatasourceOptions) -> DatasourceDefinition: +def define_datasource( + name: str, options: dict[str, Any] | DatasourceOptions +) -> DatasourceDefinition: if not NAME_PATTERN.match(name): raise ValueError( f'Invalid datasource name: "{name}". Must start with a letter or underscore and contain only alphanumeric characters and underscores.' @@ -133,9 +135,13 @@ def define_datasource(name: str, options: dict[str, Any] | DatasourceOptions) -> gcs=gcs, ) - ingestion_count = sum(1 for x in [normalized.kafka, normalized.s3, normalized.gcs] if x is not None) + ingestion_count = sum( + 1 for x in [normalized.kafka, normalized.s3, normalized.gcs] if x is not None + ) if ingestion_count > 1: - raise ValueError("Datasource can only define one ingestion option: `kafka`, `s3`, or `gcs`.") + raise ValueError( + "Datasource can only define one ingestion option: `kafka`, `s3`, or `gcs`." + ) if normalized.backfill not in {None, "skip"}: raise ValueError('Invalid datasource backfill value: only "skip" is supported.') @@ -149,7 +155,11 @@ def define_datasource(name: str, options: dict[str, Any] | DatasourceOptions) -> raise ValueError(f'Invalid datasource index "{index.name}": expr is required.') if not index.type.strip(): raise ValueError(f'Invalid datasource index "{index.name}": type is required.') - if isinstance(index.granularity, bool) or not isinstance(index.granularity, int) or index.granularity <= 0: + if ( + isinstance(index.granularity, bool) + or not isinstance(index.granularity, int) + or index.granularity <= 0 + ): raise ValueError( f'Invalid datasource index "{index.name}": granularity must be a positive integer.' ) diff --git a/src/tinybird_sdk/schema/pipe.py b/src/tinybird_sdk/schema/pipe.py index 52d5df8..1606bf5 100644 --- a/src/tinybird_sdk/schema/pipe.py +++ b/src/tinybird_sdk/schema/pipe.py @@ -159,7 +159,9 @@ def _types_are_compatible(output_type: str, datasource_type: str) -> bool: return False -def _validate_materialized_schema(pipe_name: str, output: OutputDefinition, datasource: DatasourceDefinition) -> None: +def _validate_materialized_schema( + pipe_name: str, output: OutputDefinition, datasource: DatasourceDefinition +) -> None: output_columns = list(output.keys()) datasource_columns = list(datasource._schema.keys()) @@ -196,12 +198,25 @@ def define_pipe(name: str, options: dict[str, Any] | PipeOptions) -> PipeDefinit if not normalized.nodes: raise ValueError(f'Pipe "{name}" must have at least one node.') - if (normalized.endpoint or normalized.materialized) and (not normalized.output or len(normalized.output) == 0): + if (normalized.endpoint or normalized.materialized) and ( + not normalized.output or len(normalized.output) == 0 + ): raise ValueError( f'Pipe "{name}" must have an output schema defined when used as an endpoint or materialized view.' ) - type_count = len([x for x in (normalized.endpoint, normalized.materialized, normalized.copy, normalized.sink) if x]) + type_count = len( + [ + x + for x in ( + normalized.endpoint, + normalized.materialized, + normalized.copy, + normalized.sink, + ) + if x + ] + ) if type_count > 1: raise ValueError( f'Pipe "{name}" can only have one of: endpoint, materialized, copy, or sink configuration. A pipe must be at most one type.' @@ -284,7 +299,7 @@ def _normalize_sink_config(raw: dict[str, Any]) -> SinkConfig: strategy=raw.get("strategy"), compression=raw.get("compression"), ) - raise ValueError(f"Sink connection must be a Kafka or S3 connection definition.") + raise ValueError("Sink connection must be a Kafka or S3 connection definition.") def define_sink_pipe(name: str, options: dict[str, Any]) -> PipeDefinition: diff --git a/src/tinybird_sdk/schema/project.py b/src/tinybird_sdk/schema/project.py index 571e284..f2cec69 100644 --- a/src/tinybird_sdk/schema/project.py +++ b/src/tinybird_sdk/schema/project.py @@ -2,7 +2,7 @@ from dataclasses import dataclass, field import os -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, cast from .datasource import DatasourceDefinition from .pipe import PipeDefinition, get_endpoint_config @@ -57,7 +57,13 @@ def truncate(self, options: dict[str, Any] | None = None) -> dict[str, Any]: class _PipeAccessor: - def __init__(self, get_client: Callable[[], TinybirdClient], pipe_key: str, pipe_name: str, endpoint_enabled: bool): + def __init__( + self, + get_client: Callable[[], TinybirdClient], + pipe_key: str, + pipe_name: str, + endpoint_enabled: bool, + ): self._get_client = get_client self._pipe_key = pipe_key self._pipe_name = pipe_name @@ -68,7 +74,7 @@ def query(self, params: dict[str, Any] | None = None) -> QueryResult: raise ValueError( f'Pipe "{self._pipe_key}" is not exposed as an endpoint. Set "endpoint: true" in the pipe definition to enable querying.' ) - return self._get_client().query(self._pipe_name, params or {}) + return cast("QueryResult", self._get_client().query(self._pipe_name, params or {})) class Tinybird: @@ -178,7 +184,7 @@ def client(self) -> TinybirdClient: return self.__client def sql(self, sql_query: str, options: dict[str, Any] | None = None) -> QueryResult: - return self.__get_client().sql(sql_query, options or {}) + return cast("QueryResult", self.__get_client().sql(sql_query, options or {})) @dataclass(frozen=True, slots=True) diff --git a/src/tinybird_sdk/schema/types.py b/src/tinybird_sdk/schema/types.py index b2184ad..0df4d4c 100644 --- a/src/tinybird_sdk/schema/types.py +++ b/src/tinybird_sdk/schema/types.py @@ -172,11 +172,15 @@ def json(self) -> TypeValidator: return TypeValidator(dict, "JSON") def enum8(self, *values: str) -> TypeValidator: - mapping = ", ".join(f"'{self._escape_enum_value(v)}' = {idx + 1}" for idx, v in enumerate(values)) + mapping = ", ".join( + f"'{self._escape_enum_value(v)}' = {idx + 1}" for idx, v in enumerate(values) + ) return TypeValidator(str, f"Enum8({mapping})") def enum16(self, *values: str) -> TypeValidator: - mapping = ", ".join(f"'{self._escape_enum_value(v)}' = {idx + 1}" for idx, v in enumerate(values)) + mapping = ", ".join( + f"'{self._escape_enum_value(v)}' = {idx + 1}" for idx, v in enumerate(values) + ) return TypeValidator(str, f"Enum16({mapping})") def ipv4(self) -> TypeValidator: diff --git a/tests/test_api_parity.py b/tests/test_api_parity.py index 4374761..6f13da0 100644 --- a/tests/test_api_parity.py +++ b/tests/test_api_parity.py @@ -130,7 +130,9 @@ def test_append_requires_either_url_or_file() -> None: api.append_datasource("events", {}) -def test_ingest_does_not_retry_503_when_max_retries_is_undefined(monkeypatch: pytest.MonkeyPatch) -> None: +def test_ingest_does_not_retry_503_when_max_retries_is_undefined( + monkeypatch: pytest.MonkeyPatch, +) -> None: attempts = 0 def fake_fetch(_url: str, **_kwargs: Any) -> _FakeResponse: @@ -232,7 +234,9 @@ def fake_fetch(_url: str, **_kwargs: Any) -> _FakeResponse: def test_ingest_raises_for_invalid_max_retries(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(api_module, "tinybird_fetch", lambda *_args, **_kwargs: _FakeResponse(200, {})) + monkeypatch.setattr( + api_module, "tinybird_fetch", lambda *_args, **_kwargs: _FakeResponse(200, {}) + ) api = TinybirdApi({"base_url": "https://api.tinybird.co", "token": "p.token"}) with pytest.raises(ValueError, match="'maxRetries' must be a finite number"): diff --git a/tests/test_cli_config_parity.py b/tests/test_cli_config_parity.py index e47216a..ea75bcb 100644 --- a/tests/test_cli_config_parity.py +++ b/tests/test_cli_config_parity.py @@ -47,12 +47,17 @@ def test_find_config_file_python_priority(tmp_path: Path) -> None: assert found["path"].endswith("tinybird.config.py") -def test_load_config_loads_dotenv_local_before_dotenv(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_load_config_loads_dotenv_local_before_dotenv( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: project = tmp_path / "project" project.mkdir() (project / ".env.local").write_text("TINYBIRD_TOKEN=local_token\n", encoding="utf-8") - (project / ".env").write_text("TINYBIRD_TOKEN=env_token\nTINYBIRD_URL=https://api.us-east-1.aws.tinybird.co\n", encoding="utf-8") + (project / ".env").write_text( + "TINYBIRD_TOKEN=env_token\nTINYBIRD_URL=https://api.us-east-1.aws.tinybird.co\n", + encoding="utf-8", + ) (project / "tinybird.config.json").write_text( json.dumps( { @@ -137,7 +142,9 @@ def test_load_config_falls_back_to_tinyb_file_when_config_and_env_are_missing( assert loaded["base_url"] == "https://api.eu-central-1.aws.tinybird.co" -def test_load_config_raises_when_no_token_sources_are_available(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_load_config_raises_when_no_token_sources_are_available( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: project = tmp_path / "project" project.mkdir() diff --git a/tests/test_cli_entrypoint.py b/tests/test_cli_entrypoint.py index 8ea9f1c..fe2c493 100644 --- a/tests/test_cli_entrypoint.py +++ b/tests/test_cli_entrypoint.py @@ -43,7 +43,9 @@ def fake_main(_args: list[str], _prog_name: str) -> None: assert cli_index._run_installed_tinybird_cli(["build"]) == 5 -def test_run_installed_tinybird_cli_errors_when_dependency_missing(monkeypatch: pytest.MonkeyPatch) -> None: +def test_run_installed_tinybird_cli_errors_when_dependency_missing( + monkeypatch: pytest.MonkeyPatch, +) -> None: _mute_output(monkeypatch) real_import = builtins.__import__ @@ -57,7 +59,11 @@ def fake_import(name: str, *args, **kwargs): def test_cli_entrypoint_delegates_non_sdk_commands(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(cli_index, "_run_installed_tinybird_cli", lambda argv: 7 if argv == ["build", "--dry-run"] else 1) + monkeypatch.setattr( + cli_index, + "_run_installed_tinybird_cli", + lambda argv: 7 if argv == ["build", "--dry-run"] else 1, + ) monkeypatch.setattr( cli_index, "run_generate", @@ -73,16 +79,22 @@ def test_cli_entrypoint_delegates_non_sdk_commands(monkeypatch: pytest.MonkeyPat def test_cli_entrypoint_delegates_empty_argv(monkeypatch: pytest.MonkeyPatch) -> None: calls: list[list[str]] = [] - monkeypatch.setattr(cli_index, "_run_installed_tinybird_cli", lambda argv: calls.append(list(argv)) or 0) + monkeypatch.setattr( + cli_index, "_run_installed_tinybird_cli", lambda argv: calls.append(list(argv)) or 0 + ) assert cli_index.main([]) == 0 assert calls == [[]] -def test_cli_entrypoint_runs_generate_locally(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: +def test_cli_entrypoint_runs_generate_locally( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: monkeypatch.setattr( cli_index, "_run_installed_tinybird_cli", - lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("should not delegate generate")), + lambda *_args, **_kwargs: (_ for _ in ()).throw( + AssertionError("should not delegate generate") + ), ) monkeypatch.setattr( cli_index, @@ -106,11 +118,15 @@ def test_cli_entrypoint_runs_generate_locally(monkeypatch: pytest.MonkeyPatch, c assert "Generated 2 resources (1 datasources, 1 pipes, 0 connections)" in out -def test_cli_entrypoint_runs_migrate_locally(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: +def test_cli_entrypoint_runs_migrate_locally( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: monkeypatch.setattr( cli_index, "_run_installed_tinybird_cli", - lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("should not delegate migrate")), + lambda *_args, **_kwargs: (_ for _ in ()).throw( + AssertionError("should not delegate migrate") + ), ) monkeypatch.setattr( cli_index, @@ -131,11 +147,15 @@ def test_cli_entrypoint_runs_migrate_locally(monkeypatch: pytest.MonkeyPatch, ca assert "Written to: /tmp/tinybird.migration.py" in out -def test_cli_entrypoint_runs_generate_json_output(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: +def test_cli_entrypoint_runs_generate_json_output( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: monkeypatch.setattr( cli_index, "_run_installed_tinybird_cli", - lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("should not delegate generate")), + lambda *_args, **_kwargs: (_ for _ in ()).throw( + AssertionError("should not delegate generate") + ), ) monkeypatch.setattr( cli_index, @@ -163,7 +183,9 @@ def test_cli_entrypoint_generate_failure_returns_error(monkeypatch: pytest.Monke monkeypatch.setattr( cli_index, "_run_installed_tinybird_cli", - lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("should not delegate generate")), + lambda *_args, **_kwargs: (_ for _ in ()).throw( + AssertionError("should not delegate generate") + ), ) monkeypatch.setattr( cli_index, @@ -178,7 +200,9 @@ def test_cli_entrypoint_migrate_failure_returns_error(monkeypatch: pytest.Monkey monkeypatch.setattr( cli_index, "_run_installed_tinybird_cli", - lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("should not delegate migrate")), + lambda *_args, **_kwargs: (_ for _ in ()).throw( + AssertionError("should not delegate migrate") + ), ) monkeypatch.setattr( cli_index, diff --git a/tests/test_cli_generate.py b/tests/test_cli_generate.py index b219856..96f8970 100644 --- a/tests/test_cli_generate.py +++ b/tests/test_cli_generate.py @@ -81,9 +81,15 @@ def test_run_generate_writes_artifacts_to_output_dir(tmp_path, monkeypatch) -> N result = run_generate({"cwd": str(tmp_path), "output_dir": str(output_dir)}) assert result.success is True - assert (output_dir / "datasources" / "events.datasource").read_text(encoding="utf-8") == "SCHEMA >" - assert (output_dir / "pipes" / "events_endpoint.pipe").read_text(encoding="utf-8") == "TYPE endpoint" - assert (output_dir / "connections" / "kafka_main.connection").read_text(encoding="utf-8") == "TYPE kafka" + assert (output_dir / "datasources" / "events.datasource").read_text( + encoding="utf-8" + ) == "SCHEMA >" + assert (output_dir / "pipes" / "events_endpoint.pipe").read_text( + encoding="utf-8" + ) == "TYPE endpoint" + assert (output_dir / "connections" / "kafka_main.connection").read_text( + encoding="utf-8" + ) == "TYPE kafka" def test_run_generate_returns_error_when_build_fails(monkeypatch) -> None: diff --git a/tests/test_cli_support.py b/tests/test_cli_support.py index fc76900..fca610c 100644 --- a/tests/test_cli_support.py +++ b/tests/test_cli_support.py @@ -18,7 +18,11 @@ def test_region_selector_prefers_env_match(monkeypatch: pytest.MonkeyPatch) -> N "fetch_regions", lambda: [ TinybirdRegion(name="EU (GCP)", api_host="https://api.tinybird.co", provider="gcp"), - TinybirdRegion(name="US East (AWS)", api_host="https://api.us-east-1.aws.tinybird.co", provider="aws"), + TinybirdRegion( + name="US East (AWS)", + api_host="https://api.us-east-1.aws.tinybird.co", + provider="aws", + ), ], ) monkeypatch.setenv("TINYBIRD_REGION", "US East (AWS)") diff --git a/tests/test_cli_workflows.py b/tests/test_cli_workflows.py index b7953ee..84217de 100644 --- a/tests/test_cli_workflows.py +++ b/tests/test_cli_workflows.py @@ -39,12 +39,18 @@ def test_init_build_and_deploy_workflow(tmp_path: Path, monkeypatch: pytest.Monk assert build_result.build.stats["datasource_count"] >= 1 assert build_result.build.stats["pipe_count"] >= 1 - monkeypatch.setattr(deploy_cmd, "deploy_to_main", lambda *_args, **_kwargs: {"success": True, "result": "success"}) + monkeypatch.setattr( + deploy_cmd, + "deploy_to_main", + lambda *_args, **_kwargs: {"success": True, "result": "success"}, + ) deploy_result = run_deploy({"cwd": str(tmp_path), "check": True}) assert deploy_result.success is True -def test_pull_migrate_and_dev_once_workflow(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_pull_migrate_and_dev_once_workflow( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.setenv("TINYBIRD_TOKEN", "p.workspace") monkeypatch.setenv("TINYBIRD_URL", "https://api.tinybird.co") @@ -95,10 +101,16 @@ def test_pull_migrate_and_dev_once_workflow(tmp_path: Path, monkeypatch: pytest. 'SCHEMA >\n id Int32\n\nENGINE "MergeTree"\nENGINE_SORTING_KEY "id"\n', encoding="utf-8", ) - migrate_result = run_migrate({"cwd": str(tmp_path), "patterns": ["legacy.datasource"], "dry_run": True}) + migrate_result = run_migrate( + {"cwd": str(tmp_path), "patterns": ["legacy.datasource"], "dry_run": True} + ) assert migrate_result["success"] is True assert migrate_result["output_content"] is not None - monkeypatch.setattr(dev_cmd, "run_build", lambda *_args, **_kwargs: type("R", (), {"success": True, "duration_ms": 1})()) + monkeypatch.setattr( + dev_cmd, + "run_build", + lambda *_args, **_kwargs: type("R", (), {"success": True, "duration_ms": 1})(), + ) dev_result = dev_cmd.run_dev({"cwd": str(tmp_path), "once": True}) assert dev_result["success"] is True diff --git a/tests/test_client_parity.py b/tests/test_client_parity.py index 0b7ce13..434626f 100644 --- a/tests/test_client_parity.py +++ b/tests/test_client_parity.py @@ -24,7 +24,9 @@ class FakeApi: def __init__(self, config: dict[str, Any]): self.config = config - def query(self, pipe_name: str, params: dict[str, Any], options: dict[str, Any]) -> dict[str, Any]: + def query( + self, pipe_name: str, params: dict[str, Any], options: dict[str, Any] + ) -> dict[str, Any]: if pipe_name == "boom": raise TinybirdApiError("boom", 500, '{"error":"boom"}', {"error": "boom"}) return {"data": [{"ok": True}], "pipe": pipe_name, "params": params} @@ -75,7 +77,9 @@ def test_tokens_namespace_wraps_token_api_errors(monkeypatch: pytest.MonkeyPatch monkeypatch.setattr( client_tokens, "create_jwt", - lambda *_args, **_kwargs: (_ for _ in ()).throw(TokenApiError("not allowed", 403, {"error": "x"})), + lambda *_args, **_kwargs: (_ for _ in ()).throw( + TokenApiError("not allowed", 403, {"error": "x"}) + ), ) client = TinybirdClient({"base_url": "https://api.tinybird.co", "token": "workspace_token"}) diff --git a/tests/test_codegen_parity.py b/tests/test_codegen_parity.py index 39d031b..04493e1 100644 --- a/tests/test_codegen_parity.py +++ b/tests/test_codegen_parity.py @@ -2,8 +2,19 @@ from pathlib import Path -from tinybird_sdk.api.resources import DatasourceColumn, DatasourceEngine, DatasourceInfo, PipeInfo, PipeNode, PipeParam -from tinybird_sdk.codegen.index import generate_client_file, generate_datasources_file, generate_pipes_file +from tinybird_sdk.api.resources import ( + DatasourceColumn, + DatasourceEngine, + DatasourceInfo, + PipeInfo, + PipeNode, + PipeParam, +) +from tinybird_sdk.codegen.index import ( + generate_client_file, + generate_datasources_file, + generate_pipes_file, +) from tinybird_sdk.codegen.type_mapper import clickhouse_type_to_validator, param_type_to_validator from tinybird_sdk.codegen.utils import format_sql_for_template, generate_engine_code, to_camel_case @@ -38,10 +49,15 @@ def _sample_resources() -> tuple[list[DatasourceInfo], list[PipeInfo]]: def test_type_mapper_and_utils_parity_behaviors() -> None: - assert clickhouse_type_to_validator("Nullable(LowCardinality(String))") == "t.string().low_cardinality().nullable()" + assert ( + clickhouse_type_to_validator("Nullable(LowCardinality(String))") + == "t.string().low_cardinality().nullable()" + ) assert param_type_to_validator("Int32", 10, required=False) == "p.int32().optional(10)" assert to_camel_case("class") == "_class" - assert "engine.replacing_merge_tree" in generate_engine_code({"type": "ReplacingMergeTree", "sorting_key": "id", "ver": "v"}) + assert "engine.replacing_merge_tree" in generate_engine_code( + {"type": "ReplacingMergeTree", "sorting_key": "id", "ver": "v"} + ) assert format_sql_for_template("{x}") == "{{x}}" @@ -49,9 +65,13 @@ def test_codegen_golden_fixtures() -> None: datasources, pipes = _sample_resources() expected_base = Path("tests/fixtures/codegen/expected") - expected_datasources = expected_base.joinpath("datasources.py").read_text(encoding="utf-8").rstrip() + "\n" + expected_datasources = ( + expected_base.joinpath("datasources.py").read_text(encoding="utf-8").rstrip() + "\n" + ) expected_pipes = expected_base.joinpath("pipes.py").read_text(encoding="utf-8").rstrip() + "\n" - expected_client = expected_base.joinpath("client.py").read_text(encoding="utf-8").rstrip() + "\n" + expected_client = ( + expected_base.joinpath("client.py").read_text(encoding="utf-8").rstrip() + "\n" + ) assert generate_datasources_file(datasources) == expected_datasources assert generate_pipes_file(pipes, datasources) == expected_pipes diff --git a/tests/test_generator_parity.py b/tests/test_generator_parity.py index 2d2a82d..a2bf7f7 100644 --- a/tests/test_generator_parity.py +++ b/tests/test_generator_parity.py @@ -3,7 +3,10 @@ from pathlib import Path from tinybird_sdk.generator.client import generate_client_file -from tinybird_sdk.generator.include_paths import get_include_watch_directories, resolve_include_files +from tinybird_sdk.generator.include_paths import ( + get_include_watch_directories, + resolve_include_files, +) from tinybird_sdk.generator.index import build_from_include from tinybird_sdk.generator.loader import load_entities @@ -51,7 +54,7 @@ def test_load_entities_and_build_from_include_with_raw_datafiles(tmp_path: Path) datasources, pipes = _write_schema_files(tmp_path) raw_datasource = tmp_path / "legacy.datasource" raw_pipe = tmp_path / "legacy.pipe" - raw_datasource.write_text("SCHEMA >\n id Int32\n_engine \"MergeTree\"\n", encoding="utf-8") + raw_datasource.write_text('SCHEMA >\n id Int32\n_engine "MergeTree"\n', encoding="utf-8") raw_pipe.write_text("NODE endpoint\n_sql >\n %\n SELECT 1 as id\n", encoding="utf-8") entities = load_entities( diff --git a/tests/test_infer_parity.py b/tests/test_infer_parity.py index a13fd0a..35cbb29 100644 --- a/tests/test_infer_parity.py +++ b/tests/test_infer_parity.py @@ -11,14 +11,19 @@ def test_infer_helpers() -> None: ds = define_datasource( "events", - {"schema": {"id": t.int32(), "name": t.string()}, "engine": {"type": "MergeTree", "sorting_key": ["id"]}}, + { + "schema": {"id": t.int32(), "name": t.string()}, + "engine": {"type": "MergeTree", "sorting_key": ["id"]}, + }, ) pipe = define_pipe( "top_events", { "params": {"limit": p.int32().optional(10)}, - "nodes": [node({"name": "n", "sql": "SELECT id, name FROM events LIMIT {{Int32(limit, 10)}}"})], + "nodes": [ + node({"name": "n", "sql": "SELECT id, name FROM events LIMIT {{Int32(limit, 10)}}"}) + ], "output": {"id": t.int32(), "name": t.string()}, "endpoint": True, }, diff --git a/tests/test_migrate_parity.py b/tests/test_migrate_parity.py index 2e4e460..86ec431 100644 --- a/tests/test_migrate_parity.py +++ b/tests/test_migrate_parity.py @@ -17,7 +17,9 @@ def test_discover_resource_files_from_directory_fixture() -> None: def test_parse_resource_file_kinds() -> None: fixture_dir = Path("tests/fixtures/migrate/input").resolve() - discovered = discover_resource_files(["*.connection", "*.datasource", "*.pipe"], str(fixture_dir)) + discovered = discover_resource_files( + ["*.connection", "*.datasource", "*.pipe"], str(fixture_dir) + ) assert discovered.errors == [] parsed = [parse_resource_file(item) for item in discovered.resources] diff --git a/tests/test_phase1_schema_generator_parity.py b/tests/test_phase1_schema_generator_parity.py index 4e86c60..5799da5 100644 --- a/tests/test_phase1_schema_generator_parity.py +++ b/tests/test_phase1_schema_generator_parity.py @@ -108,7 +108,9 @@ def test_define_datasource_validates_index_name_and_granularity() -> None: "events", { "schema": {"id": t.int32()}, - "indexes": [{"name": "bad name", "expr": "id", "type": "set(10)", "granularity": 1}], + "indexes": [ + {"name": "bad name", "expr": "id", "type": "set(10)", "granularity": 1} + ], }, ) @@ -127,7 +129,7 @@ def test_generate_datasource_ignores_non_string_json_path(monkeypatch: pytest.Mo monkeypatch.setattr( "tinybird_sdk.generator.datasource.get_column_json_path", - lambda *_args, **_kwargs: (lambda _value: "$.bad"), + lambda *_args, **_kwargs: lambda _value: "$.bad", ) generated = generate_datasource(datasource) diff --git a/tests/test_phase3_migrate_parser_parity.py b/tests/test_phase3_migrate_parser_parity.py index fa7c0a9..bf1f2d2 100644 --- a/tests/test_phase3_migrate_parser_parity.py +++ b/tests/test_phase3_migrate_parser_parity.py @@ -64,7 +64,10 @@ def test_parse_connection_supports_multiline_ssl_ca_pem() -> None: ) assert parsed.connection_type == "kafka" - assert parsed.ssl_ca_pem == "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAM\n-----END CERTIFICATE-----" + assert ( + parsed.ssl_ca_pem + == "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAM\n-----END CERTIFICATE-----" + ) def test_parse_connection_supports_multiline_ssl_ca_pem_with_following_directives() -> None: @@ -87,7 +90,10 @@ def test_parse_connection_supports_multiline_ssl_ca_pem_with_following_directive ) ) - assert parsed.ssl_ca_pem == "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAM\n-----END CERTIFICATE-----" + assert ( + parsed.ssl_ca_pem + == "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAM\n-----END CERTIFICATE-----" + ) assert parsed.security_protocol == "SASL_SSL" assert parsed.key == "mykey" @@ -273,7 +279,9 @@ def test_parse_pipe_supports_export_write_strategy_alias() -> None: def test_parse_pipe_rejects_export_directives_for_non_sink() -> None: - with pytest.raises(MigrationParseError, match=r"EXPORT_\* directives are only supported for TYPE sink"): + with pytest.raises( + MigrationParseError, match=r"EXPORT_\* directives are only supported for TYPE sink" + ): parse_pipe_file( _resource( "pipe", diff --git a/tests/test_phase4_migrate_runner_emitter_parity.py b/tests/test_phase4_migrate_runner_emitter_parity.py index e7148d8..87344c0 100644 --- a/tests/test_phase4_migrate_runner_emitter_parity.py +++ b/tests/test_phase4_migrate_runner_emitter_parity.py @@ -96,7 +96,7 @@ def test_run_migrate_maps_import_directives_to_gcs_when_connection_is_gcs(tmp_pa "\n".join( [ "TYPE gcs", - "GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON '{\"project\":\"demo\"}'", + 'GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON \'{"project":"demo"}\'', ] ), encoding="utf-8", @@ -160,7 +160,9 @@ def test_run_migrate_rejects_kafka_datasource_with_non_kafka_connection(tmp_path ) assert result.success is False - assert any("kafka ingestion requires a kafka connection" in error.message for error in result.errors) + assert any( + "kafka ingestion requires a kafka connection" in error.message for error in result.errors + ) def test_run_migrate_rejects_sink_connection_type_mismatch(tmp_path: Path) -> None: @@ -228,7 +230,7 @@ def test_run_migrate_emits_default_expr_for_sql_function_defaults(tmp_path: Path assert result.success is True assert result.errors == [] assert result.output_content is not None - assert '\'id\': t.uuid().default_expr("generateUUIDv4()"),' in result.output_content + assert "'id': t.uuid().default_expr(\"generateUUIDv4()\")," in result.output_content assert "'payload': t.string().default('{}')," in result.output_content diff --git a/tests/test_resources_pull_parity.py b/tests/test_resources_pull_parity.py index 21ef914..4653093 100644 --- a/tests/test_resources_pull_parity.py +++ b/tests/test_resources_pull_parity.py @@ -10,7 +10,9 @@ class _FakeResponse: - def __init__(self, status_code: int, payload: dict[str, Any] | None = None, text: str | None = None): + def __init__( + self, status_code: int, payload: dict[str, Any] | None = None, text: str | None = None + ): self.status_code = status_code self._payload = payload or {} self._text = text if text is not None else "" @@ -44,7 +46,9 @@ def fake_fetch(url: str, **_kwargs: Any) -> _FakeResponse: return _FakeResponse(404) monkeypatch.setattr(resources, "tinybird_fetch", fake_fetch) - pulled = resources.pull_all_resource_files({"base_url": "https://api.tinybird.co", "token": "p.token"}) + pulled = resources.pull_all_resource_files( + {"base_url": "https://api.tinybird.co", "token": "p.token"} + ) assert len(pulled["datasources"]) == 1 assert len(pulled["pipes"]) == 1 @@ -61,10 +65,14 @@ def fake_fetch(url: str, **_kwargs: Any) -> _FakeResponse: return _FakeResponse(404) monkeypatch.setattr(resources, "tinybird_fetch", fake_fetch) - assert resources.list_pipes_v1({"base_url": "https://api.tinybird.co", "token": "p.token"}) == ["top_events"] + assert resources.list_pipes_v1({"base_url": "https://api.tinybird.co", "token": "p.token"}) == [ + "top_events" + ] -def test_run_pull_writes_datasource_pipe_and_connection(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_run_pull_writes_datasource_pipe_and_connection( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.setattr( pull_cmd, "load_config_async", @@ -139,7 +147,9 @@ def test_run_pull_respects_overwrite_flag(tmp_path: Path, monkeypatch: pytest.Mo target = out / "events.datasource" target.write_text("old-content", encoding="utf-8") - not_overwritten = pull_cmd.run_pull({"cwd": str(tmp_path), "output_dir": "out", "overwrite": False}) + not_overwritten = pull_cmd.run_pull( + {"cwd": str(tmp_path), "output_dir": "out", "overwrite": False} + ) assert not_overwritten.success is False assert not_overwritten.error is not None assert "File already exists" in not_overwritten.error diff --git a/tests/test_schema_validations.py b/tests/test_schema_validations.py index 6df1ad8..03d9d39 100644 --- a/tests/test_schema_validations.py +++ b/tests/test_schema_validations.py @@ -1,4 +1,11 @@ -from tinybird_sdk import define_datasource, define_pipe, define_endpoint, define_materialized_view, node, t +from tinybird_sdk import ( + define_datasource, + define_pipe, + define_endpoint, + define_materialized_view, + node, + t, +) def test_define_datasource_name_validation() -> None: diff --git a/uv.lock b/uv.lock index ff378da..9457c78 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 1 requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] [[package]] name = "aiofiles" @@ -20,6 +24,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, ] +[[package]] +name = "ast-serialize" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157, upload-time = "2026-05-17T17:48:29.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520, upload-time = "2026-05-17T17:47:30.831Z" }, + { url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779, upload-time = "2026-05-17T17:47:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750, upload-time = "2026-05-17T17:47:34.731Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942, upload-time = "2026-05-17T17:47:36.287Z" }, + { url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517, upload-time = "2026-05-17T17:47:38.17Z" }, + { url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081, upload-time = "2026-05-17T17:47:39.826Z" }, + { url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910, upload-time = "2026-05-17T17:47:41.369Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678, upload-time = "2026-05-17T17:47:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603, upload-time = "2026-05-17T17:47:46.256Z" }, + { url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332, upload-time = "2026-05-17T17:47:47.899Z" }, + { url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979, upload-time = "2026-05-17T17:47:50.942Z" }, + { url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002, upload-time = "2026-05-17T17:47:54.093Z" }, + { url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231, upload-time = "2026-05-17T17:47:56.311Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668, upload-time = "2026-05-17T17:47:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075, upload-time = "2026-05-17T17:48:00.35Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347, upload-time = "2026-05-17T17:48:01.753Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380, upload-time = "2026-05-17T17:48:03.738Z" }, + { url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879, upload-time = "2026-05-17T17:48:05.463Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529, upload-time = "2026-05-17T17:48:07.008Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560, upload-time = "2026-05-17T17:48:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172, upload-time = "2026-05-17T17:48:09.922Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072, upload-time = "2026-05-17T17:48:11.469Z" }, + { url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488, upload-time = "2026-05-17T17:48:13.575Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702, upload-time = "2026-05-17T17:48:15.141Z" }, + { url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182, upload-time = "2026-05-17T17:48:16.779Z" }, + { url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410, upload-time = "2026-05-17T17:48:18.527Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587, upload-time = "2026-05-17T17:48:20.133Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171, upload-time = "2026-05-17T17:48:21.921Z" }, + { url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668, upload-time = "2026-05-17T17:48:23.544Z" }, + { url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311, upload-time = "2026-05-17T17:48:25.027Z" }, + { url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931, upload-time = "2026-05-17T17:48:26.591Z" }, + { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" }, +] + [[package]] name = "black" version = "26.3.0" @@ -164,6 +208,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 }, ] +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.5" @@ -329,6 +382,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/23/b28f4a03650512efff13a8fcbb977bac178a765c5a887a6720bee13fa85b/cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2", size = 2671839 }, ] +[[package]] +name = "distlib" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/8d/873e9252ea2c0e0c857884e0a2899ec43ade132345df1925ef24cbe64f18/distlib-0.4.2.tar.gz", hash = "sha256:baeb401c90f27acd15c4861ae0847d1e731c27ac3dbf4210643ba61fa1e813db", size = 614914, upload-time = "2026-06-08T16:24:15.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/aa891c893821d4d127292ed66c6940d1d715894bd5a0ce048056bc641773/distlib-0.4.2-py2.py3-none-any.whl", hash = "sha256:ca4cb11e5d746b5ec13c199cbf19ae27a241f89702b54e153a74332955446067", size = 470510, upload-time = "2026-06-08T16:24:13.208Z" }, +] + [[package]] name = "docker" version = "7.1.0" @@ -343,6 +405,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774 }, ] +[[package]] +name = "filelock" +version = "3.29.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/f9/f38573ed5844586db374d085911740a501ccfa373b455fc9413f09f85237/filelock-3.29.1.tar.gz", hash = "sha256:d97e6b1b9757569626c58caa07dc4beb1613f4a2938b1e8cc81afca398906c9e", size = 59335, upload-time = "2026-06-03T15:19:04.053Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/a0/614c5fe402fd88951df45f4dda2fa3b4e17a99ecd92340771929169b3b95/filelock-3.29.1-py3-none-any.whl", hash = "sha256:85199dfd706869641b72b2e8955d5416a4b2b7dc4b0e8e6d97b4cc1299a6983b", size = 40750, upload-time = "2026-06-03T15:19:02.959Z" }, +] + [[package]] name = "gitdb" version = "4.0.12" @@ -379,6 +450,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/2d/2f1b0a780b8c948c06c74c8c80e68ac354da52397ba432a1c5ac1923c3af/humanfriendly-8.2-py2.py3-none-any.whl", hash = "sha256:e78960b31198511f45fd455534ae7645a6207d33e512d2e842c766d15d9c8080", size = 86271 }, ] +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -406,6 +486,79 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419 }, ] +[[package]] +name = "librt" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092, upload-time = "2026-05-10T18:15:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035, upload-time = "2026-05-10T18:15:36.242Z" }, + { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022, upload-time = "2026-05-10T18:15:37.56Z" }, + { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273, upload-time = "2026-05-10T18:15:39.182Z" }, + { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083, upload-time = "2026-05-10T18:15:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139, upload-time = "2026-05-10T18:15:41.934Z" }, + { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442, upload-time = "2026-05-10T18:15:43.206Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230, upload-time = "2026-05-10T18:15:44.761Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231, upload-time = "2026-05-10T18:15:46.308Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585, upload-time = "2026-05-10T18:15:47.629Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509, upload-time = "2026-05-10T18:15:49.157Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628, upload-time = "2026-05-10T18:15:50.345Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122, upload-time = "2026-05-10T18:15:52.068Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, + { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, + { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, + { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, + { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, + { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, + { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, +] + [[package]] name = "logfire-api" version = "4.2.0" @@ -415,6 +568,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3e/e2/09a38123f47e34eaa9d252eec9dbd3c4e98f8b280c443d0ad6e06fae7ee5/logfire_api-4.2.0-py3-none-any.whl", hash = "sha256:3abbacf9a8cad13449d887e3f059f1d5968af2674a27218548fd7523e3b31c1d", size = 87813 }, ] +[[package]] +name = "mypy" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/a1/639f3024794a2a15899cb90707fe02e044c4412794c39c5769fd3df2e2ef/mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41", size = 14691685, upload-time = "2026-05-11T18:33:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/3b/08/9a585dea4325f20d8b80dc78623fa50d1fd2173b710f6237afd6ba6ab39b/mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca", size = 13555165, upload-time = "2026-05-11T18:32:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/81/dc/7c42cc9c6cb01e8eb09961f1f738741d3e9c7e9d5c5b30ec69222625cd5f/mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538", size = 13994376, upload-time = "2026-05-11T18:32:39.256Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/285946c33bce716e082c11dfeee9ee196eaf1f5042efb3581a31f9f205e4/mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398", size = 14864618, upload-time = "2026-05-11T18:34:49.765Z" }, + { url = "https://files.pythonhosted.org/packages/2b/83/82397f48af6c27e295d57979ded8490c9829040152cf7571b2f026aeb9a0/mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563", size = 15102063, upload-time = "2026-05-11T18:34:05.855Z" }, + { url = "https://files.pythonhosted.org/packages/40/68/b02dec39057b88eb03dc0aa854732e26e8361f34f9d0e20c7614967d1eba/mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389", size = 11060564, upload-time = "2026-05-11T18:35:36.494Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a8/ea3dcbef31f99b634f2ee23bb0321cbc8c1b388b76a861eb849f13c347dc/mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666", size = 9966983, upload-time = "2026-05-11T18:37:14.139Z" }, + { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, + { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, + { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, + { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, + { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, +] + [[package]] name = "mypy-extensions" version = "1.1.0" @@ -424,6 +628,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "packaging" version = "23.2" @@ -460,6 +673,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, ] +[[package]] +name = "pre-commit" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.48" @@ -835,6 +1064,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, ] +[[package]] +name = "ruff" +version = "0.15.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" }, + { url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" }, + { url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" }, + { url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" }, + { url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" }, + { url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" }, + { url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" }, +] + [[package]] name = "s3transfer" version = "0.16.0" @@ -944,14 +1198,22 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "mypy" }, + { name = "pre-commit" }, { name = "pytest" }, + { name = "ruff" }, ] [package.metadata] requires-dist = [{ name = "tinybird", specifier = "==4.5.0" }] [package.metadata.requires-dev] -dev = [{ name = "pytest", specifier = ">=9.0.2" }] +dev = [ + { name = "mypy", specifier = ">=1.18.2" }, + { name = "pre-commit", specifier = ">=4.3.0" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "ruff", specifier = ">=0.13.2" }, +] [[package]] name = "toposort" @@ -1010,6 +1272,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225 }, ] +[[package]] +name = "virtualenv" +version = "20.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/b3/b9fe4d783105896f9038c170e4630a9976b92769f951c04634774c10acb1/virtualenv-20.21.1.tar.gz", hash = "sha256:4c104ccde994f8b108163cf9ba58f3d11511d9403de87fb9b4f52bf33dbc8668", size = 12108081, upload-time = "2023-04-19T20:44:11.241Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/9b/445412c769289c18a5f9c2e8c816c1369f6c5fae4802f2196e5aef7630fd/virtualenv-20.21.1-py3-none-any.whl", hash = "sha256:09ddbe1af0c8ed2bb4d6ed226b9e6415718ad18aef9fa0ba023d96b7a8356049", size = 8744928, upload-time = "2023-04-19T20:44:07.197Z" }, +] + [[package]] name = "watchdog" version = "6.0.0"