From 561531150e7a474302de79671eb5e61f9791d9c4 Mon Sep 17 00:00:00 2001 From: nlathia Date: Wed, 17 Jun 2026 09:24:29 +0100 Subject: [PATCH 1/4] add comment to ci.yml --- .github/workflows/ci.yml | 58 +++++++++++++++++++++++++++++++++++++ .github/workflows/test.yaml | 31 -------------------- pyproject.toml | 4 +-- uv.lock | 10 +++---- 4 files changed, 65 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/test.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c98b400 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: CI + +on: + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + ci: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.DEPENDABOT_APP_ID }} + private-key: ${{ secrets.DEPENDABOT_APP_PRIVATE_KEY }} + owner: gradientlabs-ai + + - name: Configure git to use HTTPS for gradientlabs-ai repos + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + git config --global url."https://x-access-token:${GH_TOKEN}@github.com/gradientlabs-ai/".insteadOf "ssh://git@github.com/gradientlabs-ai/" + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: uv sync + + - name: Check formatting + run: uvx ruff format --check . + + - name: Lint + run: uvx ruff check . + + - name: Run unit tests + run: | + if [ -d "tests" ]; then + uv run pytest tests/ -m unit + rc=$? + [ $rc -eq 0 ] || [ $rc -eq 5 ] || exit $rc # rc=5 means no tests collected; treat as success + else + echo "No tests directory, skipping" + fi diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index 0c182e0..0000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: Run Python Tests - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: Check out - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v5 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version-file: "pyproject.toml" - - - name: Add PyTest - run: uv add pytest - - - name: Run tests - run: uv run pytest tests diff --git a/pyproject.toml b/pyproject.toml index 924e642..d32b2ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "gradient-labs" -version = "0.11.8" +version = "0.12.0" description = "Python bindings for the Gradient Labs API" readme = "README.md" requires-python = ">=3.9,<4.0" @@ -23,7 +23,7 @@ members = ["examples/tools"] [tool.uv] constraint-dependencies = [ "urllib3>=2.7.0", - "starlette>=0.49.1", + "starlette>=1.3.1", "h11>=0.16.0", "idna>=3.15", ] diff --git a/uv.lock b/uv.lock index e2aef30..30e9dbb 100644 --- a/uv.lock +++ b/uv.lock @@ -10,7 +10,7 @@ members = [ constraints = [ { name = "h11", specifier = ">=0.16.0" }, { name = "idna", specifier = ">=3.15" }, - { name = "starlette", specifier = ">=0.49.1" }, + { name = "starlette", specifier = ">=1.3.1" }, { name = "urllib3", specifier = ">=2.7.0" }, ] @@ -128,7 +128,7 @@ wheels = [ [[package]] name = "gradient-labs" -version = "0.11.8" +version = "0.12.0" source = { editable = "." } dependencies = [ { name = "dataclasses-json" }, @@ -339,14 +339,14 @@ wheels = [ [[package]] name = "starlette" -version = "0.52.1" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] [[package]] From ab8b6b370369cb10e380dab7f6374a3ddbb04922 Mon Sep 17 00:00:00 2001 From: nlathia Date: Wed, 17 Jun 2026 10:47:37 +0100 Subject: [PATCH 2/4] fix CI not running on stacked PRs Remove the branches: main filter from the pull_request trigger so CI runs on all PRs regardless of their base branch. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 8 +- pyproject.toml | 2 +- .../_conversation_add_message.py | 2 +- src/gradient_labs/client.py | 171 +++++++++++++++++- src/gradient_labs/webhook.py | 2 +- 5 files changed, 170 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c98b400..7ccd69b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,11 @@ name: CI on: - pull_request: + push: branches: - main + pull_request: + types: [opened, synchronize, reopened, ready_for_review] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -50,9 +52,7 @@ jobs: - name: Run unit tests run: | if [ -d "tests" ]; then - uv run pytest tests/ -m unit - rc=$? - [ $rc -eq 0 ] || [ $rc -eq 5 ] || exit $rc # rc=5 means no tests collected; treat as success + uv run pytest tests/ -m unit || [ $? -eq 5 ] # exit 5 means no tests selected; treat as success else echo "No tests directory, skipping" fi diff --git a/pyproject.toml b/pyproject.toml index d32b2ea..66510f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,4 +29,4 @@ constraint-dependencies = [ ] [tool.ruff.lint] -ignore = ["D100", "D101"] +ignore = ["D100", "D101", "F403", "F405"] diff --git a/src/gradient_labs/_conversation_add_message.py b/src/gradient_labs/_conversation_add_message.py index e69e7ba..78a45af 100644 --- a/src/gradient_labs/_conversation_add_message.py +++ b/src/gradient_labs/_conversation_add_message.py @@ -1,4 +1,4 @@ -from typing import Optional, Any, List +from typing import Optional, List from datetime import datetime from dataclasses import dataclass, field diff --git a/src/gradient_labs/client.py b/src/gradient_labs/client.py index 0d4c167..9654292 100644 --- a/src/gradient_labs/client.py +++ b/src/gradient_labs/client.py @@ -28,8 +28,6 @@ start_outbound_conversation, StartOutboundConversationParams, StartOutboundConversationResponse, - CustomerSource, - SupportPlatform, ) from ._handoff_target_upsert import upsert_hand_off_target, UpsertHandOffTargetParams @@ -104,18 +102,52 @@ ) from ._traffic_group_target_delete import delete_traffic_group_target + +from ._voice_call_context_read import ( + read_voice_call_context, + VoiceCallContextReadParams, +) +from .voice_call_context import VoiceCallContext + +from .back_office_task import ( + BackOfficeTask, +) +from ._back_office_task_create import ( + create_back_office_task, + BackOfficeTaskCreateParams, +) +from ._back_office_task_read import read_back_office_task + +from .terminology_substitution import TerminologySubstitution +from ._terminology_substitution_create import ( + create_terminology_substitution, + TerminologySubstitutionCreateParams, +) +from ._terminology_substitutions_list import list_terminology_substitutions +from ._terminology_substitution_read import read_terminology_substitution +from ._terminology_substitution_update import ( + update_terminology_substitution, + TerminologySubstitutionUpdateParams, +) +from ._terminology_substitution_delete import delete_terminology_substitution + +from ._traffic_group_exclusion_create import ( + create_traffic_group_exclusion, + TrafficGroupExclusionCreateParams, +) +from ._traffic_group_exclusion_delete import delete_traffic_group_exclusion + +from .ip_addresses import IPAddresses +from ._ip_addresses_list import list_ip_addresses + + from ._http_client import HttpClient, API_BASE_URL from .tool import * from .note import Note from .secret import Secret -from .resource_type import ResourceType, Scope, RefreshStrategy, SourceConfig +from .resource_type import ResourceType from .resource_source import ( ResourceSource, - SourceType, - SchemaUpdateStrategy, - ResourceHTTPDefinition, - ResourceHTTPBodyDefinition, - ResourceWebhookDefinition, ) from .webhook import Webhook, WebhookEvent @@ -797,3 +829,126 @@ def delete_traffic_group_target( traffic_group_id=traffic_group_id, target_id=target_id, ) + + # ==================== Voice Call Context ==================== + + def read_voice_call_context( + self, + *, + phone_number: str, + params: Optional[VoiceCallContextReadParams] = None, + ) -> VoiceCallContext: + """read_voice_call_context returns the most recent voice call context for a + given phone number, for use in data-dip integrations with contact-centre platforms.""" + return read_voice_call_context( + client=self.http_client, + phone_number=phone_number, + params=params, + ) + + # ==================== Back-Office Task Operations ==================== + + def create_back_office_task( + self, *, params: BackOfficeTaskCreateParams + ) -> BackOfficeTask: + """create_back_office_task creates a new back-office task routed to a + configurable back-office agent.""" + return create_back_office_task( + client=self.http_client, + params=params, + ) + + def read_back_office_task(self, *, task_id: str) -> BackOfficeTask: + """read_back_office_task retrieves a back-office task by its external ID.""" + return read_back_office_task( + client=self.http_client, + task_id=task_id, + ) + + # ==================== Terminology Substitution Operations ==================== + + def create_terminology_substitution( + self, *, params: TerminologySubstitutionCreateParams + ) -> TerminologySubstitution: + """create_terminology_substitution creates a new terminology substitution. + + Note: requires a `Management` API key.""" + return create_terminology_substitution( + client=self.http_client, + params=params, + ) + + def list_terminology_substitutions(self) -> List[TerminologySubstitution]: + """list_terminology_substitutions returns all terminology substitutions. + + Note: requires a `Management` API key.""" + return list_terminology_substitutions(client=self.http_client) + + def read_terminology_substitution( + self, *, substitution_id: str + ) -> TerminologySubstitution: + """read_terminology_substitution retrieves a terminology substitution by ID. + + Note: requires a `Management` API key.""" + return read_terminology_substitution( + client=self.http_client, + substitution_id=substitution_id, + ) + + def update_terminology_substitution( + self, *, substitution_id: str, params: TerminologySubstitutionUpdateParams + ) -> TerminologySubstitution: + """update_terminology_substitution updates an existing terminology substitution. + + Note: requires a `Management` API key.""" + return update_terminology_substitution( + client=self.http_client, + substitution_id=substitution_id, + params=params, + ) + + def delete_terminology_substitution(self, *, substitution_id: str) -> None: + """delete_terminology_substitution permanently deletes a terminology substitution. + + Note: requires a `Management` API key.""" + delete_terminology_substitution( + client=self.http_client, + substitution_id=substitution_id, + ) + + # ==================== Traffic Group Exclusions ==================== + + def create_traffic_group_exclusion( + self, + *, + group_id: str, + params: TrafficGroupExclusionCreateParams, + ) -> TrafficGroupTarget: + """create_traffic_group_exclusion prevents a procedure from being selected for + conversations in a traffic group, even when the procedure is unassigned/global. + + Note: requires a `Management` API key.""" + return create_traffic_group_exclusion( + client=self.http_client, + group_id=group_id, + params=params, + ) + + def delete_traffic_group_exclusion(self, *, group_id: str, target_id: str) -> None: + """delete_traffic_group_exclusion removes an exclusion from a traffic group. + + Note: requires a `Management` API key.""" + delete_traffic_group_exclusion( + client=self.http_client, + group_id=group_id, + target_id=target_id, + ) + + # ==================== IP Addresses ==================== + + def list_ip_addresses(self) -> IPAddresses: + """list_ip_addresses returns the CIDR ranges used by the API and the egress IP. + + Useful for customers who need to whitelist Gradient Labs IPs in their firewall + or security-group rules.""" + return list_ip_addresses(client=self.http_client) diff --git a/src/gradient_labs/webhook.py b/src/gradient_labs/webhook.py index 707f845..325685c 100644 --- a/src/gradient_labs/webhook.py +++ b/src/gradient_labs/webhook.py @@ -77,7 +77,7 @@ def parse_signature_header( timestamp, signatures = cls._get_timestamp_and_signatures( header, cls.SCHEME ) - except: + except Exception: return WebhookSignature(valid=False, timestamp=None) signed_payload = "%d.%s" % (timestamp, payload) From d486df3acf72ce41708021061eb76bb84ac9d4c9 Mon Sep 17 00:00:00 2001 From: nlathia Date: Wed, 17 Jun 2026 12:34:04 +0100 Subject: [PATCH 3/4] Fix formatting and unused import in client.py after restack conflict resolution Co-Authored-By: Claude Sonnet 4.6 --- src/gradient_labs/client.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/gradient_labs/client.py b/src/gradient_labs/client.py index 9654292..26a6039 100644 --- a/src/gradient_labs/client.py +++ b/src/gradient_labs/client.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, List +from typing import Optional, List from ._article_delete import delete_article from ._article_set_status import set_article_usage_status, SetArticleUsageStatusParams @@ -519,9 +519,7 @@ def set_procedure_gated_version( params=params, ) - def unset_procedure_gated_version( - self, *, procedure_id: str, version: int - ) -> None: + def unset_procedure_gated_version(self, *, procedure_id: str, version: int) -> None: """unset_procedure_gated_version removes gated status from a version. Once unset, the version will no longer be used for A/B testing or served as a gated version. @@ -765,9 +763,7 @@ def delete_resource_source(self, *, id: str) -> None: # ==================== Traffic Group Operations ==================== - def create_traffic_group( - self, *, params: CreateTrafficGroupParams - ) -> TrafficGroup: + def create_traffic_group(self, *, params: CreateTrafficGroupParams) -> TrafficGroup: """create_traffic_group creates a new traffic group. Note: requires a `Management` API key.""" From 57b631ba6e6789e40364d4943d271053cc0b07de Mon Sep 17 00:00:00 2001 From: nlathia Date: Wed, 17 Jun 2026 12:38:33 +0100 Subject: [PATCH 4/4] Remove module imports from client.py that belong to sibling stacked branches These imports and methods (voice_call_context, back_office_task, terminology_substitution, traffic_group_exclusion, ip_addresses) were incorrectly included during the restack conflict resolution. The files they reference only exist in sibling branches (#123-#129), not in the ancestry of this branch. Co-Authored-By: Claude Sonnet 4.6 --- src/gradient_labs/client.py | 162 ------------------------------------ 1 file changed, 162 deletions(-) diff --git a/src/gradient_labs/client.py b/src/gradient_labs/client.py index 26a6039..fb9ac7c 100644 --- a/src/gradient_labs/client.py +++ b/src/gradient_labs/client.py @@ -102,45 +102,6 @@ ) from ._traffic_group_target_delete import delete_traffic_group_target - -from ._voice_call_context_read import ( - read_voice_call_context, - VoiceCallContextReadParams, -) -from .voice_call_context import VoiceCallContext - -from .back_office_task import ( - BackOfficeTask, -) -from ._back_office_task_create import ( - create_back_office_task, - BackOfficeTaskCreateParams, -) -from ._back_office_task_read import read_back_office_task - -from .terminology_substitution import TerminologySubstitution -from ._terminology_substitution_create import ( - create_terminology_substitution, - TerminologySubstitutionCreateParams, -) -from ._terminology_substitutions_list import list_terminology_substitutions -from ._terminology_substitution_read import read_terminology_substitution -from ._terminology_substitution_update import ( - update_terminology_substitution, - TerminologySubstitutionUpdateParams, -) -from ._terminology_substitution_delete import delete_terminology_substitution - -from ._traffic_group_exclusion_create import ( - create_traffic_group_exclusion, - TrafficGroupExclusionCreateParams, -) -from ._traffic_group_exclusion_delete import delete_traffic_group_exclusion - -from .ip_addresses import IPAddresses -from ._ip_addresses_list import list_ip_addresses - - from ._http_client import HttpClient, API_BASE_URL from .tool import * from .note import Note @@ -825,126 +786,3 @@ def delete_traffic_group_target( traffic_group_id=traffic_group_id, target_id=target_id, ) - - # ==================== Voice Call Context ==================== - - def read_voice_call_context( - self, - *, - phone_number: str, - params: Optional[VoiceCallContextReadParams] = None, - ) -> VoiceCallContext: - """read_voice_call_context returns the most recent voice call context for a - given phone number, for use in data-dip integrations with contact-centre platforms.""" - return read_voice_call_context( - client=self.http_client, - phone_number=phone_number, - params=params, - ) - - # ==================== Back-Office Task Operations ==================== - - def create_back_office_task( - self, *, params: BackOfficeTaskCreateParams - ) -> BackOfficeTask: - """create_back_office_task creates a new back-office task routed to a - configurable back-office agent.""" - return create_back_office_task( - client=self.http_client, - params=params, - ) - - def read_back_office_task(self, *, task_id: str) -> BackOfficeTask: - """read_back_office_task retrieves a back-office task by its external ID.""" - return read_back_office_task( - client=self.http_client, - task_id=task_id, - ) - - # ==================== Terminology Substitution Operations ==================== - - def create_terminology_substitution( - self, *, params: TerminologySubstitutionCreateParams - ) -> TerminologySubstitution: - """create_terminology_substitution creates a new terminology substitution. - - Note: requires a `Management` API key.""" - return create_terminology_substitution( - client=self.http_client, - params=params, - ) - - def list_terminology_substitutions(self) -> List[TerminologySubstitution]: - """list_terminology_substitutions returns all terminology substitutions. - - Note: requires a `Management` API key.""" - return list_terminology_substitutions(client=self.http_client) - - def read_terminology_substitution( - self, *, substitution_id: str - ) -> TerminologySubstitution: - """read_terminology_substitution retrieves a terminology substitution by ID. - - Note: requires a `Management` API key.""" - return read_terminology_substitution( - client=self.http_client, - substitution_id=substitution_id, - ) - - def update_terminology_substitution( - self, *, substitution_id: str, params: TerminologySubstitutionUpdateParams - ) -> TerminologySubstitution: - """update_terminology_substitution updates an existing terminology substitution. - - Note: requires a `Management` API key.""" - return update_terminology_substitution( - client=self.http_client, - substitution_id=substitution_id, - params=params, - ) - - def delete_terminology_substitution(self, *, substitution_id: str) -> None: - """delete_terminology_substitution permanently deletes a terminology substitution. - - Note: requires a `Management` API key.""" - delete_terminology_substitution( - client=self.http_client, - substitution_id=substitution_id, - ) - - # ==================== Traffic Group Exclusions ==================== - - def create_traffic_group_exclusion( - self, - *, - group_id: str, - params: TrafficGroupExclusionCreateParams, - ) -> TrafficGroupTarget: - """create_traffic_group_exclusion prevents a procedure from being selected for - conversations in a traffic group, even when the procedure is unassigned/global. - - Note: requires a `Management` API key.""" - return create_traffic_group_exclusion( - client=self.http_client, - group_id=group_id, - params=params, - ) - - def delete_traffic_group_exclusion(self, *, group_id: str, target_id: str) -> None: - """delete_traffic_group_exclusion removes an exclusion from a traffic group. - - Note: requires a `Management` API key.""" - delete_traffic_group_exclusion( - client=self.http_client, - group_id=group_id, - target_id=target_id, - ) - - # ==================== IP Addresses ==================== - - def list_ip_addresses(self) -> IPAddresses: - """list_ip_addresses returns the CIDR ranges used by the API and the egress IP. - - Useful for customers who need to whitelist Gradient Labs IPs in their firewall - or security-group rules.""" - return list_ip_addresses(client=self.http_client)