diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..abe3ad6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: pip install -e ".[dev]" + + - name: Lint + run: ruff check . + + - name: Format check + run: ruff format --check . + + - name: Type check + run: mypy src/ + + - name: Unit tests + run: pytest tests/ -v diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..9cb0c71 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,28 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +permissions: + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build tools + run: pip install hatch + + - name: Build + run: hatch build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/README.md b/README.md index 46d8b00..a7cf31d 100644 --- a/README.md +++ b/README.md @@ -251,12 +251,12 @@ data = client.api.post("/apps/myapp/builds/import", content=tarball, timeout=600 ## Streaming -The low-level `stream()` method returns the raw `httpx.Response` without -checking for error status codes. This is intentional: streaming endpoints -(logs, build output) may send partial data before an error occurs. +The `stream()` method returns the raw `httpx.Response` without checking for +error status codes. This is intentional: streaming endpoints (logs, build +output) may send partial data before an error occurs. ```python -response = client._http.stream("GET", "/apps/myapp/logs", params={"follow": "true"}) +response = client.stream("GET", "/apps/myapp/logs", params={"follow": "true"}) try: if response.status_code >= 400: print(f"Error: {response.status_code}") diff --git a/pyproject.toml b/pyproject.toml index 79601ee..5ee5a1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ classifiers = [ dependencies = [ "httpx>=0.27,<1", "pydantic>=2.0,<3", + "eval_type_backport>=0.2.0; python_version < '3.10'", ] [project.urls] diff --git a/src/convox/client.py b/src/convox/client.py index e22e04c..b8f39da 100644 --- a/src/convox/client.py +++ b/src/convox/client.py @@ -130,6 +130,23 @@ def from_cli_config( host, api_key = credentials_from_cli_config(rack=rack, config_path=config_path) return cls(host, api_key, rack=rack, retry=retry, timeout=timeout) + def stream( + self, + method: str, + path: str, + **kwargs: object, + ) -> object: + """Send a streaming request and return the raw ``httpx.Response``. + + Unlike normal API calls, this does **not** raise on 4xx/5xx status + codes. The caller must check ``response.status_code`` and call + ``response.close()`` when done. + + This is intentional: streaming endpoints (logs, build output) may + send partial data before an error occurs. + """ + return self._http.stream(method, path, **kwargs) + def close(self) -> None: self._http.close()