Skip to content

feat(connectors): Google Sheet connector - #18

Merged
kienbui1995 merged 3 commits into
mainfrom
claude/magic-work-continue-mi1sf9
Jul 25, 2026
Merged

feat(connectors): Google Sheet connector#18
kienbui1995 merged 3 commits into
mainfrom
claude/magic-work-continue-mi1sf9

Conversation

@kienbui1995

@kienbui1995 kienbui1995 commented Jul 21, 2026

Copy link
Copy Markdown
Owner

What does this PR do?

Second connector per the Month-1 roadmap (MAGIC_MONTH1_TASK_LIST.md / magic-business technical-planning, Tuần 2): reads and writes Order/Customer data in a Google Sheet, for shops that don't have a dedicated order-management system yet.

  • connectors/google_sheet/GoogleSheetConnector implementing BaseConnector. Auth via Google service account (JWT bearer through google-auth); the actual Sheets API v4 calls go through httpx (same as the Zalo connector), so credential refresh uses a small httpx-based adapter for google.auth.transport.Request instead of pulling in the separate requests library just for that one call.
  • Operations: read_range, update_range, append_row, batch_update.
  • Common Schema mapping is header-name-based for Customer/Order; Order.items round-trips as a JSON string in an items_json column since sheets have no nested types.
  • Error mapping follows Google's standard {error: {code, status, message}} shape: RESOURCE_EXHAUSTEDRateLimitError, 5xx/UNAVAILABLE/INTERNALTransientError (retried via with_retry), everything else → PermanentError.
  • connectors/README.md updated to mark google_sheet as implemented (was "sắp có").

No changes to Go core or the previously-merged connector framework (sdk/python/magic_ai_sdk/connectors/).

Related issue

Tracks Month 1, Tuần 2 (Google Sheet Connector) of the technical roadmap in magic-business/technical-planning.

Checklist

  • Tests pass — cd core && go build ./... (Go core untouched, builds clean)
  • New code has tests — 13 new tests (connectors/google_sheet/tests/test_connector.py), auth mocked (no real RSA key needed), HTTP mocked via respx, all passing
  • Documentation updated — connectors/google_sheet/README.md (Vietnamese setup guide) + connectors/README.md

Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added a Google Sheets connector to read, update, append, and batch-edit spreadsheet data.
    • Added customer/order row mapping and round-trip serialization helpers.
  • Documentation
    • Added full Google Sheets connector setup and usage docs, including supported operations and limitations.
    • Updated monorepo connector documentation to mark Google Sheets as active and expand test setup steps.
  • Tests
    • Expanded Google Sheets connector tests for HTTP requests, authentication lifecycle, error translation, and schema mapping robustness.
  • Bug Fixes
    • Tightened webhook JSON/signature decode error handling for the Zalo connector.

Second connector per MAGIC_MONTH1_TASK_LIST.md (magic-business): reads
and writes Order/Customer data in a Google Sheet, for shops without a
dedicated order-management system yet.

- Auth via Google service account (JWT bearer through google-auth);
  the actual Sheets API v4 calls go through httpx like the Zalo
  connector, so credential refresh uses a small httpx-based adapter
  for google.auth.transport.Request instead of pulling in the separate
  `requests` library just for that one call.
- Operations: read_range, update_range, append_row, batch_update
- Common Schema mapping is header-name-based (Customer/Order), with
  Order.items round-tripped as a JSON string in an items_json column
  since sheets have no nested types
- Error mapping follows Google's standard {error: {code, status,
  message}} shape: RESOURCE_EXHAUSTED -> RateLimitError, 5xx/UNAVAILABLE/
  INTERNAL -> TransientError (retried), everything else -> PermanentError
- 13 tests (auth mocked, no real RSA key needed; HTTP mocked via respx)

connectors/README.md updated to reflect google_sheet is implemented.
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Google Sheet connector

Layer / File(s) Summary
Connector setup and authentication
connectors/google_sheet/__init__.py, connectors/google_sheet/config.example.yaml, connectors/google_sheet/requirements.txt, connectors/google_sheet/connector.py, connectors/google_sheet/pytest.ini
Adds the package interface, service-account configuration, dependencies, HTTP authentication adapters, lifecycle methods, and token refresh handling.
Sheets API operations
connectors/google_sheet/connector.py, connectors/google_sheet/tests/test_connector.py
Adds authenticated requests, retries, HTTP error translation, and read, update, append, and batch-update dispatch with corresponding tests.
Common schema mapping
connectors/google_sheet/connector.py, connectors/google_sheet/tests/test_connector.py
Adds customer and order row mapping, including items_json serialization and roundtrip validation.
Documentation and development integration
connectors/google_sheet/README.md, connectors/README.md, connectors/google_sheet/tests/test_connector.py
Documents setup, usage, operations, limitations, and test commands, and updates monorepo development instructions.

Zalo error handling

Layer / File(s) Summary
Webhook parsing exceptions
connectors/zalo/connector.py, connectors/zalo/webhook.py
Uses ValueError for JSON and body-decoding failures while preserving existing invalid-input behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GoogleSheetConnector
  participant GoogleAuth
  participant GoogleSheetsAPI
  GoogleSheetConnector->>GoogleAuth: refresh service-account token when needed
  GoogleSheetConnector->>GoogleSheetsAPI: execute authenticated spreadsheet operation
  GoogleSheetsAPI-->>GoogleSheetConnector: return spreadsheet data or error
  GoogleSheetConnector-->>GoogleSheetConnector: classify errors and retry transient failures
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding a Google Sheet connector.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/magic-work-continue-mi1sf9

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@socket-security

socket-security Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedpypi/​google-auth@​2.56.289100100100100

View full report

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new Google Sheet Connector to read and write orders and customers via the Google Sheets API v4, complete with documentation, configuration examples, and comprehensive unit tests. The feedback focuses on improving the robustness and error handling of the connector. Key recommendations include replacing runtime assertions with explicit checks, handling Google authentication RefreshError and 401 Unauthorized statuses to allow proper retries, preventing resource leaks during connection failures, adding defensive type and value parsing checks, and supporting Pydantic models in the schema mapping methods.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread connectors/google_sheet/connector.py Outdated
Comment on lines +110 to +116
assert self._client is not None, "call connect() first"
token = await self._ensure_token()
headers = {"Authorization": f"Bearer {token}"}
try:
resp = await self._client.request(method, url, headers=headers, **kwargs)
except httpx.RequestError as e:
raise TransientError(f"network error calling Google Sheets API: {e}") from e

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using assert for runtime validation can be problematic because assertions are stripped when Python is run with optimization flags (-O). Replace the assertion with an explicit check and raise PermanentError. Additionally, network errors during token refresh will raise RefreshError (from google.auth.exceptions), which is not caught by httpx.RequestError. Catching RefreshError and wrapping it in TransientError ensures that temporary network blips during credential refresh are properly retried.

Suggested change
assert self._client is not None, "call connect() first"
token = await self._ensure_token()
headers = {"Authorization": f"Bearer {token}"}
try:
resp = await self._client.request(method, url, headers=headers, **kwargs)
except httpx.RequestError as e:
raise TransientError(f"network error calling Google Sheets API: {e}") from e
if self._client is None:
raise PermanentError("Connector not connected. Call connect() first.")
from google.auth.exceptions import RefreshError
try:
token = await self._ensure_token()
headers = {"Authorization": f"Bearer {token}"}
resp = await self._client.request(method, url, headers=headers, **kwargs)
except RefreshError as e:
raise TransientError(f"Google auth refresh failed: {e}") from e
except httpx.RequestError as e:
raise TransientError(f"network error calling Google Sheets API: {e}") from e

Comment on lines +121 to +132
try:
error = resp.json().get("error", {})
except (json.JSONDecodeError, ValueError):
error = {}
status = error.get("status", "")
message = error.get("message", f"HTTP {resp.status_code}")

if resp.status_code == 429 or status in _RATE_LIMIT_STATUSES:
raise RateLimitError(f"Google Sheets API rate limited: {message}")
if resp.status_code >= 500 or status in _TRANSIENT_STATUSES:
raise TransientError(f"Google Sheets API transient error: {message}")
raise PermanentError(f"Google Sheets API error ({status or resp.status_code}): {message}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If the access token expires or is revoked, the Google Sheets API will return a 401 Unauthorized status code. Currently, this is treated as a PermanentError, which prevents retries. Handling 401 by invalidating the token (self._credentials.token = None) and raising a TransientError allows the retry decorator to refresh the token and successfully retry the request.

Suggested change
try:
error = resp.json().get("error", {})
except (json.JSONDecodeError, ValueError):
error = {}
status = error.get("status", "")
message = error.get("message", f"HTTP {resp.status_code}")
if resp.status_code == 429 or status in _RATE_LIMIT_STATUSES:
raise RateLimitError(f"Google Sheets API rate limited: {message}")
if resp.status_code >= 500 or status in _TRANSIENT_STATUSES:
raise TransientError(f"Google Sheets API transient error: {message}")
raise PermanentError(f"Google Sheets API error ({status or resp.status_code}): {message}")
try:
error = resp.json().get("error", {})
except (json.JSONDecodeError, ValueError):
error = {}
status = error.get("status", "")
message = error.get("message", f"HTTP {resp.status_code}")
if resp.status_code == 401:
self._credentials.token = None
raise TransientError(f"Google Sheets API token expired or invalid: {message}")
if resp.status_code == 429 or status in _RATE_LIMIT_STATUSES:
raise RateLimitError(f"Google Sheets API rate limited: {message}")
if resp.status_code >= 500 or status in _TRANSIENT_STATUSES:
raise TransientError(f"Google Sheets API transient error: {message}")
raise PermanentError(f"Google Sheets API error ({status or resp.status_code}): {message}")

Comment on lines +91 to +94
async def connect(self) -> bool:
self._client = httpx.AsyncClient(timeout=15.0)
await self._ensure_token()
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If _ensure_token() raises an exception during connect(), the newly created httpx.AsyncClient will not be closed, leading to a resource leak. Wrap the token initialization in a try...except block to ensure the client is closed if connection fails.

Suggested change
async def connect(self) -> bool:
self._client = httpx.AsyncClient(timeout=15.0)
await self._ensure_token()
return True
async def connect(self) -> bool:
self._client = httpx.AsyncClient(timeout=15.0)
try:
await self._ensure_token()
except Exception:
await self._client.aclose()
self._client = None
raise
return True

Comment thread connectors/google_sheet/connector.py Outdated
Comment on lines +170 to +172
header: list[str] = raw_data["header"]
row: list[str] = raw_data["row"]
by_col = dict(zip(header, row + [""] * (len(header) - len(row))))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add defensive checks to ensure raw_data is a dictionary and contains the expected keys. This prevents unhandled TypeError or KeyError exceptions if the raw data format is malformed.

        if not isinstance(raw_data, dict):
            raise PermanentError("raw_data must be a dictionary")
        header: list[str] = raw_data.get("header") or []
        row: list[str] = raw_data.get("row") or []
        by_col = dict(zip(header, row + [""] * (len(header) - len(row))))

Comment on lines +187 to +207
if entity_type == "order":
items = []
items_raw = by_col.pop("items_json", "")
if items_raw:
try:
items = json.loads(items_raw)
except (json.JSONDecodeError, ValueError):
items = []
known = {f: by_col.pop(f, "") for f in _ORDER_FIELDS}
return {
"id": known["id"] or "",
"source_connector": self.name,
"external_id": known["id"] or "",
"customer_id": known["customer_id"] or None,
"status": known["status"] or "pending",
"items": items,
"total_amount": float(known["total_amount"] or 0),
"shipping_fee": float(known["shipping_fee"] or 0),
"created_at": known["created_at"] or None,
"metadata": by_col,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Ensure that items_json parses into a list of items rather than a dictionary or other JSON types, which would fail Pydantic validation later. Also, defensively handle ValueError when casting total_amount and shipping_fee to floats to prevent mapping failures due to malformed cell values.

        if entity_type == "order":
            items = []
            items_raw = by_col.pop("items_json", "")
            if items_raw:
                try:
                    parsed = json.loads(items_raw)
                    if isinstance(parsed, list):
                        items = parsed
                except (json.JSONDecodeError, ValueError):
                    pass
            known = {f: by_col.pop(f, "") for f in _ORDER_FIELDS}

            def _to_float(val: Any) -> float:
                try:
                    return float(val) if val else 0.0
                except ValueError:
                    return 0.0

            return {
                "id": known["id"] or "",
                "source_connector": self.name,
                "external_id": known["id"] or "",
                "customer_id": known["customer_id"] or None,
                "status": known["status"] or "pending",
                "items": items,
                "total_amount": _to_float(known["total_amount"]),
                "shipping_fee": _to_float(known["shipping_fee"]),
                "created_at": known["created_at"] or None,
                "metadata": by_col,
            }

Comment thread connectors/google_sheet/connector.py Outdated
Comment on lines +211 to +226
def map_from_common_schema(self, common_data: dict[str, Any], entity_type: str) -> list[str]:
if entity_type == "customer":
fields = _CUSTOMER_FIELDS
elif entity_type == "order":
fields = [*_ORDER_FIELDS, "items_json"]
else:
raise PermanentError(f"GoogleSheetConnector cannot map entity_type={entity_type!r}")

row = []
for field in fields:
if field == "items_json":
row.append(json.dumps(common_data.get("items", [])))
else:
value = common_data.get(field, "")
row.append("" if value is None else str(value))
return row

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Workflows often pass Pydantic models (like Customer or Order) instead of raw dictionaries to map_from_common_schema. Since Pydantic models do not have a .get() method, this will raise an AttributeError. Adding a check to convert Pydantic models to dictionaries makes the mapping robust. Additionally, ensure that items defaults to [] if it is None to avoid serializing it as "null".

    def map_from_common_schema(self, common_data: dict[str, Any], entity_type: str) -> list[str]:
        if not isinstance(common_data, dict):
            if hasattr(common_data, "model_dump"):
                common_data = common_data.model_dump()
            elif hasattr(common_data, "dict"):
                common_data = common_data.dict()
            else:
                raise PermanentError("common_data must be a dictionary or a Pydantic model")

        if entity_type == "customer":
            fields = _CUSTOMER_FIELDS
        elif entity_type == "order":
            fields = [*_ORDER_FIELDS, "items_json"]
        else:
            raise PermanentError(f"GoogleSheetConnector cannot map entity_type={entity_type!r}")

        row = []
        for field in fields:
            if field == "items_json":
                row.append(json.dumps(common_data.get("items") or []))
            else:
                value = common_data.get(field, "")
                row.append("" if value is None else str(value))
        return row

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
connectors/google_sheet/connector.py (2)

123-124: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove redundant exception class.

In Python, json.JSONDecodeError inherits from ValueError. Catching ValueError is sufficient to handle both json parsing errors and arbitrary invalid strings (like parsing floats).

  • connectors/google_sheet/connector.py#L123-L124: Simplify the except block to just ValueError.
  • connectors/google_sheet/connector.py#L193-L194: Simplify the except block to just ValueError.
♻️ Proposed refactor
-        except (json.JSONDecodeError, ValueError):
+        except ValueError:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@connectors/google_sheet/connector.py` around lines 123 - 124, In
connectors/google_sheet/connector.py, update both exception handlers at lines
123-124 and 193-194 to catch only ValueError, removing the redundant
json.JSONDecodeError entry while preserving the existing error handling
behavior.

Source: Linters/SAST tools


118-119: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Prefer checking is_success for HTTP responses.

While Google Sheets API heavily uses 200 OK, it is safer and more idiomatic to check for any successful 2xx status code when working with REST APIs.

♻️ Proposed refactor
-        if resp.status_code == 200:
+        if resp.is_success:
             return resp.json() if resp.content else {}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@connectors/google_sheet/connector.py` around lines 118 - 119, Update the HTTP
response status check in the Google Sheets request flow to use the response’s
is_success property instead of comparing status_code to 200, while preserving
the existing JSON-or-empty-dictionary return behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@connectors/google_sheet/connector.py`:
- Around line 111-117: Update _request so self._ensure_token() executes inside
the existing try block, allowing token-refresh httpx.RequestError exceptions to
be mapped to TransientError and handled by with_retry. Import and catch
GoogleAuthError in the same method, mapping authentication failures according to
the connector’s established auth-error handling instead of allowing them to
escape unhandled.

In `@connectors/google_sheet/README.md`:
- Around line 8-13: Update the setup instructions in the README around the
config copy and service-account JSON steps to explicitly warn that config.yaml
and the JSON key contain secrets and must not be committed or exposed. Recommend
using service_account_file or a secret manager instead of embedding the key in
config.yaml where possible.
- Around line 17-19: Update the setup and test command examples in the
google_sheet README so they use one consistent working directory. Add an
explicit change into connectors/google_sheet before relative commands, or
convert every affected install, test, and import-related path to be valid from
the repository root; apply the same correction to the sections referenced by the
comment.
- Around line 24-28: Update the Google Sheets connector documentation around the
header example and append_row usage to state that write columns must remain in
the canonical field order emitted by map_from_common_schema, or revise
serialization to use the sheet’s header order. Ensure the documented behavior
prevents reordered columns from receiving mismatched values, including the
additional affected examples.

---

Nitpick comments:
In `@connectors/google_sheet/connector.py`:
- Around line 123-124: In connectors/google_sheet/connector.py, update both
exception handlers at lines 123-124 and 193-194 to catch only ValueError,
removing the redundant json.JSONDecodeError entry while preserving the existing
error handling behavior.
- Around line 118-119: Update the HTTP response status check in the Google
Sheets request flow to use the response’s is_success property instead of
comparing status_code to 200, while preserving the existing
JSON-or-empty-dictionary return behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1a941c6f-b02a-416b-8513-dad5eff64753

📥 Commits

Reviewing files that changed from the base of the PR and between 3c064d3 and cbaf038.

📒 Files selected for processing (9)
  • connectors/README.md
  • connectors/google_sheet/README.md
  • connectors/google_sheet/__init__.py
  • connectors/google_sheet/config.example.yaml
  • connectors/google_sheet/connector.py
  • connectors/google_sheet/pytest.ini
  • connectors/google_sheet/requirements.txt
  • connectors/google_sheet/tests/__init__.py
  • connectors/google_sheet/tests/test_connector.py

Comment thread connectors/google_sheet/connector.py Outdated
Comment thread connectors/google_sheet/README.md
Comment thread connectors/google_sheet/README.md Outdated
Comment thread connectors/google_sheet/README.md
claude added 2 commits July 21, 2026 15:15
Fixes the SonarCloud "C Reliability Rating" gate failure and Gemini/
CodeRabbit review comments on PR #18:

- Replace `assert self._client is not None` with an explicit check +
  PermanentError — assertions are stripped under `python -O`, so this
  was flagged as a real reliability bug, not just style
- connect(): close and clear the httpx client if token init fails
  during connect(), instead of leaking it
- _request(): catch GoogleAuthError (covers RefreshError and friends)
  and raise AuthError — a revoked/invalid service account key isn't
  fixed by retrying, so it shouldn't go through with_retry; genuine
  network errors during token refresh still flow through the existing
  httpx.RequestError -> TransientError path since token fetch now runs
  inside the same try block as the request
- _request(): treat HTTP 401 as TransientError and clear the cached
  token so the retry is forced to refresh, instead of failing
  permanently on a token that expired mid-flight
- map_to_common_schema: reject non-dict raw_data, only accept a JSON
  list (not object/other) for items_json, and parse total_amount/
  shipping_fee defensively instead of letting a malformed cell crash
  the whole mapping
- map_from_common_schema: accept Pydantic models (via model_dump/dict)
  in addition to plain dicts, default items to [] instead of "null"
- Simplify redundant `except (json.JSONDecodeError, ValueError)` to
  just ValueError (JSONDecodeError is already a ValueError subclass);
  use resp.is_success instead of `== 200`
- README: warn that config.yaml/service-account key are secrets and
  shouldn't be committed, use repo-root-relative paths consistently,
  and document that map_from_common_schema writes in a fixed column
  order (reordering sheet columns will misalign writes)

9 new/updated tests covering the above; all passing (22 total in this
connector, 36 in sdk/python, 22 in zalo — none affected).
Root cause of the lingering "C Reliability Rating on New Code" gate on
PR #18, which persisted even after the Google Sheet connector's own
redundant excepts were fixed in 89d2db5.

UnicodeDecodeError and json.JSONDecodeError are both subclasses of
ValueError, so listing them alongside ValueError in the same except
tuple is dead weight — SonarCloud rule python:S5713, which it classifies
as a Bug (Reliability), not a code smell. It's the same rule SonarCloud
flagged on PR #17 (connector.py:163) before that PR merged.

Because the Zalo connector landed on main today (PR #17), it still falls
inside SonarCloud's New Code window, so its issues counted against
PR #18's quality gate even though PR #18 doesn't touch those files.

Behavior is unchanged: the same exceptions are still caught, just via
their common base class.

Left sdk/python/magic_ai_sdk/worker.py:131 (same pattern) untouched on
purpose — it's long-standing code outside this PR's scope, and editing
it would pull the whole file into Sonar's new-code window.

Tests: zalo 22, google_sheet 22, sdk/python 36 — all passing.
@sonarqubecloud

Copy link
Copy Markdown

@kienbui1995
kienbui1995 merged commit f4d5b16 into main Jul 25, 2026
15 of 18 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
connectors/google_sheet/tests/test_connector.py (2)

225-235: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Verify that connection failure awaits aclose().

_client is None does not prove the HTTP client was closed. Instrument the AsyncClient with an AsyncMock and assert aclose() was awaited exactly once.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@connectors/google_sheet/tests/test_connector.py` around lines 225 - 235,
Update test_connect_failure_closes_client_and_clears_it to replace the
connector’s HTTP client with an instrumented AsyncClient using an AsyncMock for
aclose(). After the expected connect failure, assert that aclose() was awaited
exactly once, while retaining the existing assertion that conn._client is None.

214-223: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Assert the connector’s retry boundaries, not only final exceptions.

Both tests can pass with incorrect retry behavior:

  • connectors/google_sheet/tests/test_connector.py#L214-L223: verify a 401 triggers another request after token refresh.
  • connectors/google_sheet/tests/test_connector.py#L243-L251: verify RefreshError causes exactly one refresh attempt and no retry.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@connectors/google_sheet/tests/test_connector.py` around lines 214 - 223,
Strengthen the retry-boundary tests in
connectors/google_sheet/tests/test_connector.py:214-223 and
connectors/google_sheet/tests/test_connector.py:243-251. In
test_401_response_clears_token_and_raises_transient, assert that token refresh
is followed by a second request before the final TransientError; in the
RefreshError test, assert exactly one refresh attempt and no retry request. Use
the existing request mocks and connector symbols without changing production
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@connectors/google_sheet/tests/test_connector.py`:
- Around line 225-235: Update test_connect_failure_closes_client_and_clears_it
to replace the connector’s HTTP client with an instrumented AsyncClient using an
AsyncMock for aclose(). After the expected connect failure, assert that aclose()
was awaited exactly once, while retaining the existing assertion that
conn._client is None.
- Around line 214-223: Strengthen the retry-boundary tests in
connectors/google_sheet/tests/test_connector.py:214-223 and
connectors/google_sheet/tests/test_connector.py:243-251. In
test_401_response_clears_token_and_raises_transient, assert that token refresh
is followed by a second request before the final TransientError; in the
RefreshError test, assert exactly one refresh attempt and no retry request. Use
the existing request mocks and connector symbols without changing production
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 244130b0-807b-422c-b2f0-b145729e8c9c

📥 Commits

Reviewing files that changed from the base of the PR and between cbaf038 and 4d4f015.

📒 Files selected for processing (5)
  • connectors/google_sheet/README.md
  • connectors/google_sheet/connector.py
  • connectors/google_sheet/tests/test_connector.py
  • connectors/zalo/connector.py
  • connectors/zalo/webhook.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • connectors/google_sheet/README.md
  • connectors/google_sheet/connector.py

kienbui1995 pushed a commit that referenced this pull request Jul 25, 2026
Tuần 2 of the Month-1 roadmap: the customer-service pipeline for Vietnamese
online shops.

    Nhận tin -> Classify Intent -> Extract Order Info -> Fetch Order Status
             -> Draft Response -> Guardrail Check -> Send hoặc Handoff

packs/ecomops/ (new):
- text.py — Vietnamese normalization. Customers routinely type without
  diacritics, so both the message and the keyword lists are folded to a
  diacritics-free form; "đơn hàng" and "don hang" match the same rule. đ/Đ
  are handled explicitly since they don't decompose under NFD.
- intents.py — 7 EcomOps intents. RuleBasedIntentClassifier matches keyword
  tokens in order allowing one filler word between them, because Vietnamese
  slots pronouns inside fixed phrases ("khi nào EM nhận được hàng" must still
  match "khi nao nhan"). A greeting only wins when it's the whole message, so
  "chào shop, đơn em đâu" classifies as order_status. LLMIntentClassifier goes
  through MagiC's own /api/v1/llm/chat gateway (cheapest routing) rather than a
  provider SDK, so spend lands in the Cost Controller. HybridIntentClassifier
  only pays for the LLM when the rules aren't confident, and keeps the rule
  guess if the gateway is down.
- extraction.py — order code + phone. Phones are masked out before the bare
  numeric order-code pattern runs, otherwise "sdt 0901234567" returns the phone
  as the order id and every lookup silently misses.
- guardrails.py — an LLM draft is not automatically safe to send from a shop's
  official account. Blocks leaked third-party contacts, hard delivery
  guarantees, unapproved refunds/vouchers, and system-prompt/credential leaks.
  Runs on normalized text so it can't be evaded by dropping diacritics.
- prompts.py — Vietnamese prompt library, kept as data so shops can retune tone
  without touching pipeline code.
- workflow.py — the pipeline. Every dependency is injected, so it makes no
  network calls and knows nothing about Zalo or Sheets. A blocked draft is
  never returned to the caller: reply falls back to a safe message and action
  becomes "handoff". Complaints skip drafting entirely.
- example_worker.py — end-to-end demo (Zalo webhook -> Sheet lookup -> guarded
  reply), which is the stated Tuần 2 deliverable.

CI (this is the part that was quietly broken): the python job only ever ran
sdk/python. The 44 connector tests added in #17/#18 have never executed on CI,
and neither would these 76. Running `pytest connectors packs` from the root
failed outright because asyncio_mode lived only in per-directory pytest.ini
files — hence the new root pytest.ini. ruff was also weaker outside sdk/python
(E501/I aren't in ruff's default rule set), so a root ruff.toml now mirrors
sdk/python's settings; the 4 import-order issues that surfaced are fixed.

Tests: 76 new (packs) + 44 (connectors) + 36 (sdk) = 156, all passing. Go core
untouched, builds clean.
kienbui1995 added a commit that referenced this pull request Jul 25, 2026
* feat(packs): EcomOps workflow + run connector/pack tests in CI

Tuần 2 of the Month-1 roadmap: the customer-service pipeline for Vietnamese
online shops.

    Nhận tin -> Classify Intent -> Extract Order Info -> Fetch Order Status
             -> Draft Response -> Guardrail Check -> Send hoặc Handoff

packs/ecomops/ (new):
- text.py — Vietnamese normalization. Customers routinely type without
  diacritics, so both the message and the keyword lists are folded to a
  diacritics-free form; "đơn hàng" and "don hang" match the same rule. đ/Đ
  are handled explicitly since they don't decompose under NFD.
- intents.py — 7 EcomOps intents. RuleBasedIntentClassifier matches keyword
  tokens in order allowing one filler word between them, because Vietnamese
  slots pronouns inside fixed phrases ("khi nào EM nhận được hàng" must still
  match "khi nao nhan"). A greeting only wins when it's the whole message, so
  "chào shop, đơn em đâu" classifies as order_status. LLMIntentClassifier goes
  through MagiC's own /api/v1/llm/chat gateway (cheapest routing) rather than a
  provider SDK, so spend lands in the Cost Controller. HybridIntentClassifier
  only pays for the LLM when the rules aren't confident, and keeps the rule
  guess if the gateway is down.
- extraction.py — order code + phone. Phones are masked out before the bare
  numeric order-code pattern runs, otherwise "sdt 0901234567" returns the phone
  as the order id and every lookup silently misses.
- guardrails.py — an LLM draft is not automatically safe to send from a shop's
  official account. Blocks leaked third-party contacts, hard delivery
  guarantees, unapproved refunds/vouchers, and system-prompt/credential leaks.
  Runs on normalized text so it can't be evaded by dropping diacritics.
- prompts.py — Vietnamese prompt library, kept as data so shops can retune tone
  without touching pipeline code.
- workflow.py — the pipeline. Every dependency is injected, so it makes no
  network calls and knows nothing about Zalo or Sheets. A blocked draft is
  never returned to the caller: reply falls back to a safe message and action
  becomes "handoff". Complaints skip drafting entirely.
- example_worker.py — end-to-end demo (Zalo webhook -> Sheet lookup -> guarded
  reply), which is the stated Tuần 2 deliverable.

CI (this is the part that was quietly broken): the python job only ever ran
sdk/python. The 44 connector tests added in #17/#18 have never executed on CI,
and neither would these 76. Running `pytest connectors packs` from the root
failed outright because asyncio_mode lived only in per-directory pytest.ini
files — hence the new root pytest.ini. ruff was also weaker outside sdk/python
(E501/I aren't in ruff's default rule set), so a root ruff.toml now mirrors
sdk/python's settings; the 4 import-order issues that surfaced are fixed.

Tests: 76 new (packs) + 44 (connectors) + 36 (sdk) = 156, all passing. Go core
untouched, builds clean.

* fix(ecomops): canonicalize VN phone forms in the leaked-contact guardrail

Found while PR #19's CI was running: a shop that configures its hotline as
"+84987654321" (or "84987654321") had its OWN number flagged as a leaked
third-party contact, so any reply quoting the hotline was blocked and pushed to
a human. The rule compared raw digit strings, so the +84 and 0-prefixed
spellings of the same number never matched.

Adds `canonical_phone()` — folds "+84 987-654-321", "84987654321",
"987654321" and "0987-654-321" all to "0987654321" — and compares canonical
forms on both sides. Detection is unaffected: a genuinely foreign number is
still blocked (covered by a new test that allowlists the hotline and checks a
different number is still caught).

Also NOSONAR the `pip install ruff` lint line. Editing that line to widen the
lint scope made SonarCloud treat it as new code and re-apply the same two
supply-chain hotspots (S4830/S5445) already suppressed on the install lines
above it — that's what dropped this PR's Security Rating to C.

Tests: 14 new (canonical_phone table + hotline spellings + allowlist scoping),
170 total, all passing.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants