diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 009b7821..ac8eac82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,3 +29,27 @@ jobs: - name: Run lints run: ./scripts/lint + + upload: + if: github.repository == 'stainless-sdks/openlayer-python' + timeout-minutes: 10 + name: upload + permissions: + contents: read + id-token: write + runs-on: depot-ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Get GitHub OIDC Token + id: github-oidc + uses: actions/github-script@v6 + with: + script: core.setOutput('github_token', await core.getIDToken()); + + - name: Upload tarball + env: + URL: https://pkg.stainless.com/s + AUTH: ${{ steps.github-oidc.outputs.github_token }} + SHA: ${{ github.sha }} + run: ./scripts/utils/upload-artifact.sh diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 5d9c21c9..fd599489 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.0-alpha.62" + ".": "0.2.0-alpha.63" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b10d5f5..d322990b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 0.2.0-alpha.63 (2025-06-03) + +Full Changelog: [v0.2.0-alpha.62...v0.2.0-alpha.63](https://github.com/openlayer-ai/openlayer-python/compare/v0.2.0-alpha.62...v0.2.0-alpha.63) + +### Features + +* add MLflow notebook example ([149e85f](https://github.com/openlayer-ai/openlayer-python/commit/149e85f075db80c9800fd8dff58b277341a3384c)) +* add OpenLIT notebook example ([f71c668](https://github.com/openlayer-ai/openlayer-python/commit/f71c66895d38b0245f8a5da4c000e6bf747ef4c8)) +* **client:** add follow_redirects request option ([87d8986](https://github.com/openlayer-ai/openlayer-python/commit/87d89863dd9c4f700b8a8910ce14d2a961404336)) + + +### Bug Fixes + +* **package:** support direct resource imports ([8407753](https://github.com/openlayer-ai/openlayer-python/commit/84077531a8491bc48c8fe5d67a9076a27ba21fce)) + + +### Chores + +* **ci:** fix installation instructions ([d7d4fd2](https://github.com/openlayer-ai/openlayer-python/commit/d7d4fd2e5464f87660a30edd1067aef930b2249a)) +* **ci:** upload sdks to package manager ([0aadb0a](https://github.com/openlayer-ai/openlayer-python/commit/0aadb0a4deed48d46981fd44b308fba5bbc5a3c1)) +* **docs:** grammar improvements ([27794bc](https://github.com/openlayer-ai/openlayer-python/commit/27794bc2ff2f34c10c1635fcf14677e0711a8af0)) +* **docs:** remove reference to rye shell ([9f8db4a](https://github.com/openlayer-ai/openlayer-python/commit/9f8db4a42a79af923d55ec636e43bf49ce80bc50)) +* **internal:** avoid errors for isinstance checks on proxies ([3de384b](https://github.com/openlayer-ai/openlayer-python/commit/3de384be80ba27ba97a6079a78b75cdeadf55e5f)) +* **internal:** codegen related update ([120114a](https://github.com/openlayer-ai/openlayer-python/commit/120114ad9d40ce7c41112522f2951dd92be61eaf)) +* **internal:** codegen related update ([f990977](https://github.com/openlayer-ai/openlayer-python/commit/f990977209f13f02b1b87ab98bef5eef50414ea9)) +* link to OpenLLMetry integration guide ([ffcd085](https://github.com/openlayer-ai/openlayer-python/commit/ffcd085e1ad58e2b88fac6f739b6a9a12ba05844)) +* remove MLflow example ([17256c9](https://github.com/openlayer-ai/openlayer-python/commit/17256c96873cef5b085400ad64af860c35de4cf4)) +* sync repo ([caa47dc](https://github.com/openlayer-ai/openlayer-python/commit/caa47dc5b9d671046dca4dd5378a72018ed5d334)) + ## 0.2.0-alpha.62 (2025-04-29) Full Changelog: [v0.2.0-alpha.61...v0.2.0-alpha.62](https://github.com/openlayer-ai/openlayer-python/compare/v0.2.0-alpha.61...v0.2.0-alpha.62) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a053ce9..da31df73 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,8 +17,7 @@ $ rye sync --all-features You can then run scripts using `rye run python script.py` or by activating the virtual environment: ```sh -$ rye shell -# or manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work +# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work $ source .venv/bin/activate # now you can omit the `rye run` prefix diff --git a/SECURITY.md b/SECURITY.md index 8614b059..dc108d01 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -16,11 +16,11 @@ before making any information public. ## Reporting Non-SDK Related Security Issues If you encounter security issues that are not directly related to SDKs but pertain to the services -or products provided by Openlayer please follow the respective company's security reporting guidelines. +or products provided by Openlayer, please follow the respective company's security reporting guidelines. ### Openlayer Terms and Policies -Please contact support@openlayer.com for any questions or concerns regarding security of our services. +Please contact support@openlayer.com for any questions or concerns regarding the security of our services. --- diff --git a/pyproject.toml b/pyproject.toml index 7333be69..99b45518 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openlayer" -version = "0.2.0-alpha.62" +version = "0.2.0-alpha.63" description = "The official Python library for the openlayer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh new file mode 100755 index 00000000..e7a0c9ec --- /dev/null +++ b/scripts/utils/upload-artifact.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -exuo pipefail + +RESPONSE=$(curl -X POST "$URL" \ + -H "Authorization: Bearer $AUTH" \ + -H "Content-Type: application/json") + +SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url') + +if [[ "$SIGNED_URL" == "null" ]]; then + echo -e "\033[31mFailed to get signed URL.\033[0m" + exit 1 +fi + +UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \ + -H "Content-Type: application/gzip" \ + --data-binary @- "$SIGNED_URL" 2>&1) + +if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then + echo -e "\033[32mUploaded build to Stainless storage.\033[0m" + echo -e "\033[32mInstallation: pip install --pre 'https://pkg.stainless.com/s/openlayer-python/$SHA'\033[0m" +else + echo -e "\033[31mFailed to upload artifact.\033[0m" + exit 1 +fi diff --git a/src/openlayer/__init__.py b/src/openlayer/__init__.py index e6918d32..8b434e24 100644 --- a/src/openlayer/__init__.py +++ b/src/openlayer/__init__.py @@ -1,5 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +import typing as _t + from . import types from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes from ._utils import file_from_path @@ -78,6 +80,9 @@ "DefaultAsyncHttpxClient", ] +if not _t.TYPE_CHECKING: + from ._utils._resources_proxy import resources as resources + _setup_logging() # Update the __module__ attribute for exported symbols so that diff --git a/src/openlayer/_base_client.py b/src/openlayer/_base_client.py index df1dab62..718469f7 100644 --- a/src/openlayer/_base_client.py +++ b/src/openlayer/_base_client.py @@ -960,6 +960,9 @@ def request( if self.custom_auth is not None: kwargs["auth"] = self.custom_auth + if options.follow_redirects is not None: + kwargs["follow_redirects"] = options.follow_redirects + log.debug("Sending HTTP Request: %s %s", request.method, request.url) response = None @@ -1460,6 +1463,9 @@ async def request( if self.custom_auth is not None: kwargs["auth"] = self.custom_auth + if options.follow_redirects is not None: + kwargs["follow_redirects"] = options.follow_redirects + log.debug("Sending HTTP Request: %s %s", request.method, request.url) response = None diff --git a/src/openlayer/_models.py b/src/openlayer/_models.py index 798956f1..4f214980 100644 --- a/src/openlayer/_models.py +++ b/src/openlayer/_models.py @@ -737,6 +737,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): idempotency_key: str json_data: Body extra_json: AnyMapping + follow_redirects: bool @final @@ -750,6 +751,7 @@ class FinalRequestOptions(pydantic.BaseModel): files: Union[HttpxRequestFiles, None] = None idempotency_key: Union[str, None] = None post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() + follow_redirects: Union[bool, None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. diff --git a/src/openlayer/_types.py b/src/openlayer/_types.py index c19dc25f..75357538 100644 --- a/src/openlayer/_types.py +++ b/src/openlayer/_types.py @@ -100,6 +100,7 @@ class RequestOptions(TypedDict, total=False): params: Query extra_json: AnyMapping idempotency_key: str + follow_redirects: bool # Sentinel class used until PEP 0661 is accepted @@ -215,3 +216,4 @@ class _GenericAlias(Protocol): class HttpxSendArgs(TypedDict, total=False): auth: httpx.Auth + follow_redirects: bool diff --git a/src/openlayer/_utils/_proxy.py b/src/openlayer/_utils/_proxy.py index ffd883e9..0f239a33 100644 --- a/src/openlayer/_utils/_proxy.py +++ b/src/openlayer/_utils/_proxy.py @@ -46,7 +46,10 @@ def __dir__(self) -> Iterable[str]: @property # type: ignore @override def __class__(self) -> type: # pyright: ignore - proxied = self.__get_proxied__() + try: + proxied = self.__get_proxied__() + except Exception: + return type(self) if issubclass(type(proxied), LazyProxy): return type(proxied) return proxied.__class__ diff --git a/src/openlayer/_utils/_resources_proxy.py b/src/openlayer/_utils/_resources_proxy.py new file mode 100644 index 00000000..d1c684e5 --- /dev/null +++ b/src/openlayer/_utils/_resources_proxy.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from typing import Any +from typing_extensions import override + +from ._proxy import LazyProxy + + +class ResourcesProxy(LazyProxy[Any]): + """A proxy for the `openlayer.resources` module. + + This is used so that we can lazily import `openlayer.resources` only when + needed *and* so that users can just import `openlayer` and reference `openlayer.resources` + """ + + @override + def __load__(self) -> Any: + import importlib + + mod = importlib.import_module("openlayer.resources") + return mod + + +resources = ResourcesProxy().__as_proxied__() diff --git a/src/openlayer/_version.py b/src/openlayer/_version.py index e013ded0..37ec914f 100644 --- a/src/openlayer/_version.py +++ b/src/openlayer/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "openlayer" -__version__ = "0.2.0-alpha.62" # x-release-please-version +__version__ = "0.2.0-alpha.63" # x-release-please-version diff --git a/tests/test_client.py b/tests/test_client.py index 265760da..7562a048 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -921,6 +921,33 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.http_request.headers.get("x-stainless-retry-count") == "42" + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects(self, respx_mock: MockRouter) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + self.client.post( + "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response + ) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" + class TestAsyncOpenlayer: client = AsyncOpenlayer(base_url=base_url, api_key=api_key, _strict_response_validation=True) @@ -1847,3 +1874,30 @@ async def test_main() -> None: raise AssertionError("calling get_platform using asyncify resulted in a hung process") time.sleep(0.1) + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects(self, respx_mock: MockRouter) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + await self.client.post( + "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response + ) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py index 7f09e39e..da6f4851 100644 --- a/tests/test_utils/test_proxy.py +++ b/tests/test_utils/test_proxy.py @@ -21,3 +21,14 @@ def test_recursive_proxy() -> None: assert dir(proxy) == [] assert type(proxy).__name__ == "RecursiveLazyProxy" assert type(operator.attrgetter("name.foo.bar.baz")(proxy)).__name__ == "RecursiveLazyProxy" + + +def test_isinstance_does_not_error() -> None: + class AlwaysErrorProxy(LazyProxy[Any]): + @override + def __load__(self) -> Any: + raise RuntimeError("Mocking missing dependency") + + proxy = AlwaysErrorProxy() + assert not isinstance(proxy, dict) + assert isinstance(proxy, LazyProxy)
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: