Skip to content

[FEATURE] Add client-driven Pact contracts for expectation suite CRUD (GX-2729)#11756

Merged
joshua-stauffer merged 2 commits intodevelopfrom
GX-2729/expectation-suite-contracts
Apr 7, 2026
Merged

[FEATURE] Add client-driven Pact contracts for expectation suite CRUD (GX-2729)#11756
joshua-stauffer merged 2 commits intodevelopfrom
GX-2729/expectation-suite-contracts

Conversation

@joshua-stauffer
Copy link
Copy Markdown
Member

Summary

  • Add test_expectation_suite_contracts.py with client-driven Pact contract tests
  • Tests cover: suites.add(), suites.get(), suites.add_or_update(), and suites.delete() through the Python client
  • V2 endpoint: /api/v2/.../expectation-suites
  • All scenario strings unique, no EachLike(minimum=0) usage

Closes GX-2729

Test plan

  • Verify all existing rest_contracts/ tests still pass
  • Verify new tests are collected by pytest
  • Verify contract JSON is generated when run with Pact broker tokens

@netlify
Copy link
Copy Markdown

netlify bot commented Mar 27, 2026

Deploy Preview for niobium-lead-7998 canceled.

Name Link
🔨 Latest commit d539998
🔍 Latest deploy log https://app.netlify.com/projects/niobium-lead-7998/deploys/69d513a1512f380008ed5cac

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.66%. Comparing base (139a2c3) to head (d539998).
⚠️ Report is 2 commits behind head on develop.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff            @@
##           develop   #11756   +/-   ##
========================================
  Coverage    84.66%   84.66%           
========================================
  Files          471      471           
  Lines        39170    39170           
========================================
  Hits         33165    33165           
  Misses        6005     6005           
Flag Coverage Δ
3.10 73.56% <ø> (ø)
3.10 athena ?
3.10 aws_deps ?
3.10 big ?
3.10 clickhouse ?
3.10 filesystem ?
3.10 mysql ?
3.10 openpyxl or pyarrow or project or sqlite or aws_creds ?
3.10 postgresql ?
3.10 spark ?
3.10 spark_connect ?
3.10 sql_server ?
3.10 trino ?
3.11 73.60% <ø> (ø)
3.11 athena ?
3.11 aws_deps ?
3.11 big ?
3.11 clickhouse ?
3.11 filesystem ?
3.11 mysql ?
3.11 openpyxl or pyarrow or project or sqlite or aws_creds ?
3.11 postgresql ?
3.11 spark ?
3.11 spark_connect ?
3.11 sql_server ?
3.11 trino ?
3.12 73.59% <ø> (ø)
3.12 athena ?
3.12 aws_deps ?
3.12 big ?
3.12 filesystem ?
3.12 mysql ?
3.12 openpyxl or pyarrow or project or sqlite or aws_creds ?
3.12 postgresql ?
3.12 spark ?
3.12 spark_connect ?
3.12 sql_server ?
3.12 trino ?
3.13 73.61% <ø> (ø)
3.13 athena 41.93% <ø> (ø)
3.13 aws_deps 45.18% <ø> (ø)
3.13 big 55.27% <ø> (ø)
3.13 bigquery 51.25% <ø> (ø)
3.13 clickhouse 41.94% <ø> (ø)
3.13 databricks 53.06% <ø> (ø)
3.13 filesystem 64.37% <ø> (ø)
3.13 gx-redshift 51.41% <ø> (ø)
3.13 mysql 51.81% <ø> (ø)
3.13 openpyxl or pyarrow or project or sqlite or aws_creds 59.97% <ø> (ø)
3.13 postgresql 55.22% <ø> (ø)
3.13 snowflake 53.90% <ø> (+<0.01%) ⬆️
3.13 spark 55.92% <ø> (ø)
3.13 spark_connect 46.85% <ø> (ø)
3.13 sql_server 53.23% <ø> (ø)
3.13 trino 48.75% <ø> (ø)
cloud 0.00% <ø> (ø)
docs-basic 59.52% <ø> (ø)
docs-creds-needed 58.11% <ø> (ø)
docs-spark 57.57% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI review requested due to automatic review settings April 2, 2026 17:36
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds new client-driven Pact contract tests exercising Great Expectations Cloud V2 REST endpoints through the Python client (covering expectation suite CRUD and fluent datasource/asset flows).

Changes:

  • Add expectation suite CRUD client-driven Pact tests for V2 /expectation-suites.
  • Add datasource CRUD client-driven Pact tests for V2 /datasources.
  • Add client-driven contract tests for embedded data-asset + batch-definition updates via datasource PUTs.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
tests/integration/cloud/rest_contracts/test_expectation_suite_contracts.py New Pact tests for expectation suite add/get/update/delete via ctx.suites.* against V2 endpoints.
tests/integration/cloud/rest_contracts/test_datasource_contracts.py New Pact tests for fluent datasource CRUD via ctx.data_sources.* against V2 endpoints.
tests/integration/cloud/rest_contracts/test_data_asset_batch_def_contracts.py New Pact tests validating datasource PUT payloads when adding assets and batch definitions (embedded resources).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +97 to +102
"""context.suites.add() issues GET (has_key probe) then POST.

Full interaction sequence:
1. GET /data-context-configuration (context init)
2. GET /expectation-suites?name=... (has_key probe — suite must not exist)
3. POST /expectation-suites (create the suite)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

ctx.suites.add() re-fetches the suite after the POST (see SuiteFactory.add: it calls self.get(name=...) after self._store.add(...)), which triggers two additional GET /expectation-suites?name=... requests (has_key + get) that must return the newly-created suite. This test only registers a single GET interaction returning an empty list, so the post-create re-fetch will not match and/or will cause the client call to fail. Register a second GET-by-name interaction for the post-POST state (returning at least one suite), or otherwise model the sequential empty→non-empty responses.

Suggested change
"""context.suites.add() issues GET (has_key probe) then POST.
Full interaction sequence:
1. GET /data-context-configuration (context init)
2. GET /expectation-suites?name=... (has_key probesuite must not exist)
3. POST /expectation-suites (create the suite)
"""context.suites.add() issues GET (has_key probe), POST, then refetches.
Full interaction sequence:
1. GET /data-context-configuration (context init)
2. GET /expectation-suites?name=... (has_key probesuite must not exist)
3. POST /expectation-suites (create the suite)
4. GET /expectation-suites?name=... (has_key probesuite must now exist)
5. GET /expectation-suites?name=... (fetch the newly created suite)

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +12
"""Client-driven Pact contract tests for datasource CRUD operations.

Each test:
1. Registers the GET /data-context-configuration interaction via
``setup_data_context_config_interaction()``.
2. Registers the datasource-specific interaction(s).
3. Constructs a ``CloudDataContext`` and exercises the Python client API inside
the ``with pact_test:`` block.
4. Asserts the client correctly parses the response.

URL pattern for datasources (V2 endpoint):
/api/v2/organizations/{org_id}/workspaces/{workspace_id}/datasources
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

The PR description says it adds test_expectation_suite_contracts.py, but this PR also introduces full client-driven contract suites for datasources (and data assets/batch definitions). Please update the PR description/title (or split into separate PRs) so reviewers know the intended scope and test coverage being added.

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +103
"""context.suites.add() issues GET (has_key probe) then POST.

Full interaction sequence:
1. GET /data-context-configuration (context init)
2. GET /expectation-suites?name=... (has_key probe — suite must not exist)
3. POST /expectation-suites (create the suite)
"""
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

SuiteFactory.add() re-fetches the suite after the POST by calling self.get(name=...), which triggers additional GET /expectation-suites?name=... requests that must return the newly-created suite (non-empty data). This test only registers the pre-create has_key probe that returns an empty list, so the post-POST re-fetch will not match any interaction. Register follow-up GET-by-name interaction(s) after the POST (empty -> non-empty) to reflect the real call sequence.

Copilot uses AI. Check for mistakes.
Comment on lines +287 to +306
"""add_batch_definition_whole_dataframe() issues retrieve_by_name then PUT + GET.

Adding a batch definition requires a ``retrieve_by_name`` call on the
datasource store, which makes two identical ``GET /datasources?name=``
requests (``has_key`` probe + actual get). Pact v2 reuses a single
registered interaction for both, so only one by-name interaction is
registered. After each PUT, ``_persist_datasource`` re-fetches using
both id (in the URL path) and name (as a query parameter).

Full interaction sequence:
1. GET /data-context-configuration (context init)
2. GET /datasources (existence check before create)
3. POST /datasources (create the parent datasource)
4. GET /datasources/{id}?name=... (post-POST refresh)
5. PUT /datasources/{id} (add DataFrameAsset to datasource)
6. GET /datasources/{id}?name=... (post-PUT refresh after asset add)
7. GET /datasources?name=... (serves both retrieve_by_name calls for batch def)
8. PUT /datasources/{id} (add BatchDefinition to datasource)
9. GET /datasources/{id}?name=... (post-PUT refresh — primary contract)
"""
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

DataAsset.add_batch_definition_*() calls Datasource.add_batch_definition(), which invokes data_context.update_datasource(...). In Cloud mode this triggers an additional GET /datasources (list) existence check and then a re-fetch by name (GET /datasources?name=...) after the PUT (via data_sources.all()[name]). These interactions aren’t registered here, so the mock server won’t be able to satisfy the real request sequence (and the contract won’t reflect actual client behavior). Add the missing list/by-name interactions with appropriate pre/post-update responses.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +13
"""Client-driven Pact contract tests for datasource CRUD operations.

Each test:
1. Registers the GET /data-context-configuration interaction via
``setup_data_context_config_interaction()``.
2. Registers the datasource-specific interaction(s).
3. Constructs a ``CloudDataContext`` and exercises the Python client API inside
the ``with pact_test:`` block.
4. Asserts the client correctly parses the response.

URL pattern for datasources (V2 endpoint):
/api/v2/organizations/{org_id}/workspaces/{workspace_id}/datasources
"""
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

The PR description states it adds only expectation-suite contract tests, but this PR also introduces datasource CRUD contract tests and data-asset/batch-definition contract tests under tests/integration/cloud/rest_contracts/. Please update the PR description/title to match the actual scope (or split into separate PRs) so reviewers and release notes accurately reflect the changes.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 6, 2026 11:42
@joshua-stauffer joshua-stauffer force-pushed the GX-2729/expectation-suite-contracts branch from b6c91e7 to 67c4d08 Compare April 6, 2026 11:42
@joshua-stauffer joshua-stauffer changed the base branch from develop to GX-2728/data-asset-batch-def-contracts April 6, 2026 11:42
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

.with_request("POST", SUITES_PATH)
.with_headers(headers)
.with_body(request_body, content_type="application/json")
.will_respond_with(201)
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

The POST interaction is recorded as returning HTTP 201, but all other V2 cloud create-contract tests in this suite (e.g., datasources) expect HTTP 200 for successful POSTs. If the provider returns 200 here, this Pact will fail provider verification even though the client accepts any 2xx. Please align the expected status code with the actual API behavior (and keep it consistent with the other REST contract tests unless this endpoint is intentionally different).

Suggested change
.will_respond_with(201)
.will_respond_with(200)

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +68
_SUITE_RESPONSE: Final[dict] = {
"id": EXISTING_SUITE_ID,
"organization_id": match.uuid(),
"created_by_id": match.uuid(),
"name": SUITE_NAME,
"expectations": [],
"meta": {"great_expectations_version": match.like("1.0.0")},
"notes": None,
}

# Suite payload that includes one expectation (used to validate round-trip).
_SUITE_WITH_EXPECTATION_RESPONSE: Final[dict] = {
"id": EXISTING_SUITE_ID,
"organization_id": match.uuid(),
"created_by_id": match.uuid(),
"name": SUITE_NAME,
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

The response-body matchers require organization_id and created_by_id to be present, but the Python client’s cloud response parsing for suites ignores unknown/extra fields (see ExpectationSuiteDTO in great_expectations/data_context/store/expectations_store.py, which only models id, name, expectations, meta, notes). Including unused fields in the Pact contract can over-constrain the provider and cause verification failures if those fields are omitted/renamed without impacting the client. Consider removing these fields from _SUITE_RESPONSE / _SUITE_WITH_EXPECTATION_RESPONSE (or only asserting fields the client actually needs).

Copilot uses AI. Check for mistakes.
# ---------------------------------------------------------------------------


def _session_headers() -> dict:
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

_session_headers() is missing the more specific return type used across the other contract test modules (they use dict[str, str]). Updating the signature here keeps typing consistent and avoids leaking non-string header value types if create_session() changes.

Suggested change
def _session_headers() -> dict:
def _session_headers() -> dict[str, str]:

Copilot uses AI. Check for mistakes.
@joshua-stauffer joshua-stauffer force-pushed the GX-2728/data-asset-batch-def-contracts branch from 6a5570b to bf7a89e Compare April 6, 2026 12:18
@joshua-stauffer joshua-stauffer force-pushed the GX-2729/expectation-suite-contracts branch from 9d986bb to 6abd2c5 Compare April 6, 2026 12:19
@joshua-stauffer joshua-stauffer force-pushed the GX-2728/data-asset-batch-def-contracts branch from bf7a89e to b8f4300 Compare April 6, 2026 14:04
@joshua-stauffer joshua-stauffer force-pushed the GX-2729/expectation-suite-contracts branch from 6abd2c5 to 4803f2c Compare April 6, 2026 14:04
Base automatically changed from GX-2728/data-asset-batch-def-contracts to GX-2727/datasource-crud-contracts April 6, 2026 19:05
@joshua-stauffer joshua-stauffer changed the base branch from GX-2727/datasource-crud-contracts to develop April 7, 2026 12:02
@joshua-stauffer joshua-stauffer force-pushed the GX-2729/expectation-suite-contracts branch from fa843e5 to 93a5871 Compare April 7, 2026 12:05
Copilot AI review requested due to automatic review settings April 7, 2026 12:26
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +54 to +60
_SUITE_RESPONSE: Final[dict] = {
"id": EXISTING_SUITE_ID,
"name": SUITE_NAME,
"expectations": [],
"meta": {"great_expectations_version": match.like("1.0.0")},
"notes": None,
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

The suite response payload hard-codes the suite id value. That makes the contract overly restrictive (provider verification must return this exact UUID), and it’s inconsistent with the expectation payload below which uses match.uuid(). Consider using a UUID matcher (or at least match.like(<uuid>)) for the suite id field to keep the contract resilient while still validating the shape.

Copilot uses AI. Check for mistakes.
Comment on lines +357 to +367
pact_test.upon_receiving("a request to update an expectation suite via PUT (client-driven)")
.given("an expectation suite exists for update")
.with_request("PUT", SUITE_BY_ID_PATH)
.with_headers(headers)
.with_body(put_request_body, content_type="application/vnd.api+json")
.will_respond_with(200)
.with_body(
{"data": match.like(_SUITE_WITH_EXPECTATION_RESPONSE)},
content_type="application/json",
)
)
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

This contract registers an update interaction as a successful PUT. However, the GXCloudStoreBackend currently falls back to PATCH for expectation suites when the provider returns 405 on PUT (see great_expectations/data_context/store/gx_cloud_store_backend.py:_put). If the real provider still behaves that way, provider verification will fail for this pact. Consider either (1) modeling the 405-on-PUT then PATCH retry sequence in the contract test, or (2) asserting PATCH directly if that’s the supported provider behavior.

Copilot uses AI. Check for mistakes.
joshua-stauffer and others added 2 commits April 7, 2026 16:24
…, batch def, and expectation suite CRUD (GX-2727, GX-2728, GX-2729)

Rebased onto develop to pull in pact-python CI dep updates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove test_datasource_contracts.py and test_data_asset_batch_def_contracts.py
  (belong to upstream GX-2727/GX-2728 PRs)
- Revert abstract_data_context.py change (upstream concern)
- Pin invoke==3.0.0 to fix CI static-analysis failures
- Fix _session_headers return type to dict[str, str]
- Remove organization_id/created_by_id from suite response matchers
  (client ignores these fields via Extra.ignore)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 7, 2026 14:24
@joshua-stauffer joshua-stauffer force-pushed the GX-2729/expectation-suite-contracts branch from 409f551 to d539998 Compare April 7, 2026 14:24
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

.with_query_parameters({"name": SUITE_NAME})
.will_respond_with(200)
.with_body(
{"data": match.each_like(match.like(_SUITE_RESPONSE), min=1)},
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

The client-side deserialization for ExpectationsStore.gx_cloud_response_json_to_object_dict() raises if a GET ...expectation-suites?name=... response contains more than one item. Using match.each_like(..., min=1) allows the provider to return multiple suites and still satisfy the contract, which would mask a real incompatibility. Prefer matching a single-element list so the contract enforces exactly one suite for a name lookup.

Suggested change
{"data": match.each_like(match.like(_SUITE_RESPONSE), min=1)},
{"data": [match.like(_SUITE_RESPONSE)]},

Copilot uses AI. Check for mistakes.
.with_query_parameters({"name": SUITE_NAME})
.will_respond_with(200)
.with_body(
{"data": match.each_like(match.like(_SUITE_WITH_EXPECTATION_RESPONSE), min=1)},
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

Same issue as the name-lookup GET in test_get_expectation_suite_by_name: ExpectationsStore.gx_cloud_response_json_to_object_dict() requires exactly one suite when data is a list. match.each_like(..., min=1) permits multiple results and would allow a provider response that the client would reject. Match a single-item list instead.

Suggested change
{"data": match.each_like(match.like(_SUITE_WITH_EXPECTATION_RESPONSE), min=1)},
{"data": [match.like(_SUITE_WITH_EXPECTATION_RESPONSE)]},

Copilot uses AI. Check for mistakes.
.with_query_parameters({"name": SUITE_NAME})
.will_respond_with(200)
.with_body(
{"data": match.each_like(match.like(_SUITE_RESPONSE), min=1)},
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

For delete-by-name, the preceding GET ...expectation-suites?name=... is parsed via ExpectationsStore.gx_cloud_response_json_to_object_dict(), which raises if the response contains more than one suite. Using match.each_like(..., min=1) allows multiple suites and would make the contract pass even though the client would fail. Match a single-element list to enforce the client expectation.

Suggested change
{"data": match.each_like(match.like(_SUITE_RESPONSE), min=1)},
{"data": [match.like(_SUITE_RESPONSE)]},

Copilot uses AI. Check for mistakes.
@joshua-stauffer joshua-stauffer added this pull request to the merge queue Apr 7, 2026
Merged via the queue into develop with commit 20eed8d Apr 7, 2026
64 checks passed
@joshua-stauffer joshua-stauffer deleted the GX-2729/expectation-suite-contracts branch April 7, 2026 16:00
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.

3 participants