Skip to content

Commit 973a3c5

Browse files
committed
Merge remote-tracking branch 'great-expectations/develop' into develop
2 parents cf65c03 + b1740be commit 973a3c5

File tree

71 files changed

+753
-354
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+753
-354
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,9 +244,6 @@ jobs:
244244
# Authorize access to Google Cloud with a service account
245245
./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=$GOOGLE_APPLICATION_CREDENTIALS
246246
247-
- name: Install mssql odbc driver
248-
run: ./scripts/install_mssql_odbc_driver.sh
249-
250247
- name: Install cloud dependencies
251248
if: ${{ matrix.start_services }}
252249
run: |
@@ -710,6 +707,7 @@ jobs:
710707
invoke deps --gx-install -m '${{ matrix.markers }}' -r test
711708
712709
- name: Install mssql odbc driver
710+
if: matrix.markers == 'mssql'
713711
run: ./scripts/install_mssql_odbc_driver.sh
714712

715713
- name: Configure ECR AWS Credentials

docs/docusaurus/docs/cloud/connect/connect_snowflake.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ description: Connect GX Cloud to a Snowflake Data Source.
99
- You have a [GX Cloud account](https://greatexpectations.io/cloud) with [Workspace Editor permissions](/cloud/access/manage_access.md#roles-and-permissions) or greater.
1010
- You have a Snowflake database, schema, and table or view.
1111
- You have a [Snowflake account](https://docs.snowflake.com/en/user-guide-admin) with USAGE privileges on the table or view, database, and schema you are validating, and you have SELECT privileges on the table or view you are validating.
12-
- You have [configured and stored private and public keys](https://docs.snowflake.com/en/user-guide/key-pair-auth) for Snowflake key-pair authentication.
12+
- You have [configured and stored an unencrypted private key and public key](https://docs.snowflake.com/en/user-guide/key-pair-auth) for Snowflake key-pair authentication.
1313
:::warning Password authentication is deprecated
1414
Snowflake has deprecated password authentication and will remove support for it entirely in the future. Set up new Data Sources with key-pair authentication. If you have older Snowflake Data Sources using password authentication, update them to use key-pair authentication. For more information about the deprecation, see [Snowflake's documentation](https://docs.snowflake.com/en/user-guide/security-mfa-rollout).
1515
:::
@@ -30,7 +30,7 @@ description: Connect GX Cloud to a Snowflake Data Source.
3030

3131
- **Username**: Enter the username you use to access Snowflake.
3232

33-
- **Private key**: Enter your RSA private key value. Do not include the start and end markers `-----BEGIN/END ENCRYPTED PRIVATE KEY-----`.
33+
- **Private key**: Enter your unencrypted private key value. Do not include the start and end markers `-----BEGIN/END PRIVATE KEY-----`.
3434

3535
- **Database**: Enter the name of the Snowflake database where the data you want to validate is stored. In Snowsight, click **Data** > **Databases**. In the Snowflake Classic Console, click **Databases**.
3636

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export default {
2-
release_version: 'great_expectations, version 1.8.0',
2+
release_version: 'great_expectations, version 1.8.1',
33
min_python: '3.9',
44
max_python: '3.13'
55
}

docs/docusaurus/docs/core/connect_to_data/configure_credentials/_connection_string.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Different types of SQL databases have different formats for their connection det
77

88
Other connection string formats are valid provided they are for a SQL database that is supported by SQLAlchemy. You can find more information on the dialects supported by `SQLAlchemy` on their [dialects](https://docs.sqlalchemy.org/en/20/dialects/index.html) page.
99

10-
To connect to Snowflake, you will pass your connection details and credentials as the following parameters: `account`, `user`, `database`, `schema`, `warehouse`, `role`, and `private_key`. When setting your `private_key` value, do not include the start and end markers `-----BEGIN/END ENCRYPTED PRIVATE KEY-----`.
10+
To connect to Snowflake, you will pass your connection details and credentials as the following parameters: `account`, `user`, `database`, `schema`, `warehouse`, `role`, and `private_key`. When setting your `private_key` value, make sure to use an unencrypted key and do not include the start and end markers `-----BEGIN/END PRIVATE KEY-----`.
1111

1212
:::warning Snowflake password authentication is deprecated
1313
Snowflake has deprecated password authentication and will remove support for it entirely in the future. Set up new Data Sources with key-pair authentication. If you have older Snowflake Data Sources using password authentication, update them to use key-pair authentication. For more information about the deprecation, see [Snowflake's documentation](https://docs.snowflake.com/en/user-guide/security-mfa-rollout).

docs/docusaurus/docs/core/connect_to_data/sql_data/_create_a_data_source/_create_a_data_source.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ import DatasourceMethodReferenceTable from './_datasource_method_reference_table
6767
schema = "my_schema"
6868
warehouse = "my_wh"
6969
role = "my_role"
70-
private_key = "my_private_key"
70+
private_key = "my_unencrypted_private_key"
7171

7272
data_source = context.data_sources.add_snowflake(
7373
name=datasource_name,

docs/docusaurus/docs/oss/changelog.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,24 @@ When we deprecate public functionality, we will
1515

1616
Before we completely remove the functionality in a new major release, there will be at least one minor release that contains the deprecation so that you can smoothly transition.
1717

18+
### 1.8.1
19+
* [BUGFIX] Fix render_content generation with new row_condition types ([#11481](https://github.com/great-expectations/great_expectations/pull/11481))
20+
* [BUGFIX] Prevent unmocked HTTP requests in cloud tests ([#11492](https://github.com/great-expectations/great_expectations/pull/11492))
21+
* [BUGFIX] Update pytest filter to handle google.api_core Python 3.10 end-of-life warning ([#11493](https://github.com/great-expectations/great_expectations/pull/11493))
22+
* [DOCS] snowflake password deprecation ([#11416](https://github.com/great-expectations/great_expectations/pull/11416))
23+
* [DOCS] correct Snowflake private key guidance ([#11490](https://github.com/great-expectations/great_expectations/pull/11490))
24+
* [MAINTENANCE] Stop running python 3.12 marker tests on all PRs ([#11457](https://github.com/great-expectations/great_expectations/pull/11457))
25+
* [MAINTENANCE] Disable link checker ([#11477](https://github.com/great-expectations/great_expectations/pull/11477))
26+
* [MAINTENANCE] Transform legacy row_condition string into new Condition object ([#11474](https://github.com/great-expectations/great_expectations/pull/11474))
27+
* [MAINTENANCE] Keep `condition_parser` field intact for backwards compatibility ([#11484](https://github.com/great-expectations/great_expectations/pull/11484))
28+
* [MAINTENANCE] Replace broken bitnami image with apache image for spark ([#11444](https://github.com/great-expectations/great_expectations/pull/11444))
29+
* [MAINTENANCE] Ignore future warning Python 3.9 EOL ([#11486](https://github.com/great-expectations/great_expectations/pull/11486))
30+
* [MAINTENANCE] Add passthrough path for pandas and spark Condition parser ([#11480](https://github.com/great-expectations/great_expectations/pull/11480))
31+
* [MAINTENANCE] Constrain `row_condition` groups ([#11488](https://github.com/great-expectations/great_expectations/pull/11488))
32+
* [MAINTENANCE] Only install mssql drivers when needed ([#11489](https://github.com/great-expectations/great_expectations/pull/11489))
33+
* [MAINTENANCE] Use Suites v2 REST endpoints ([#11487](https://github.com/great-expectations/great_expectations/pull/11487))
34+
* [MAINTENANCE] Handle `None` parameter in `Condition`s ([#11491](https://github.com/great-expectations/great_expectations/pull/11491))
35+
1836
### 1.8.0
1937
* [FEATURE] Snowflake Key Pair Auth API ([#11395](https://github.com/great-expectations/great_expectations/pull/11395))
2038
* [BUGFIX] Row condition for Volume Expectations ([#11467](https://github.com/great-expectations/great_expectations/pull/11467))

docs/docusaurus/docusaurus.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ module.exports = {
306306
lastVersion: 'current',
307307
versions: {
308308
current: {
309-
label: '1.8.0',
309+
label: '1.8.1',
310310
},
311311
['0.18']: {
312312
label: '0.18.21',

great_expectations/data_context/store/gx_cloud_store_backend.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class GXCloudStoreBackend(StoreBackend, metaclass=ABCMeta):
109109
GXCloudRESTResource.DATA_ASSET: EndpointVersion.V1,
110110
GXCloudRESTResource.DATA_CONTEXT: EndpointVersion.V1,
111111
GXCloudRESTResource.DATA_CONTEXT_VARIABLES: EndpointVersion.V1,
112-
GXCloudRESTResource.EXPECTATION_SUITE: EndpointVersion.V1,
112+
GXCloudRESTResource.EXPECTATION_SUITE: EndpointVersion.V2,
113113
GXCloudRESTResource.VALIDATION_DEFINITION: EndpointVersion.V1,
114114
GXCloudRESTResource.VALIDATION_RESULT: EndpointVersion.V1,
115115
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.8.0
1+
1.8.1

great_expectations/expectations/conditions.py

Lines changed: 176 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from enum import Enum
44
from typing import TYPE_CHECKING, Any, Iterable, List, Literal, Union
55

6-
from great_expectations.compatibility.pydantic import BaseModel, Field, validator
6+
from great_expectations.compatibility.pydantic import BaseModel, Field, root_validator, validator
77
from great_expectations.compatibility.typing_extensions import override
88

99
if TYPE_CHECKING:
@@ -24,6 +24,39 @@ def __init__(self, value: Any):
2424
super().__init__(f"Invalid condition type: {type(value)}")
2525

2626

27+
class AndContainsOrError(ValueError):
28+
"""Raised when an AND group contains OR conditions."""
29+
30+
def __init__(self):
31+
super().__init__("AND groups cannot contain OR conditions")
32+
33+
34+
class NestedOrError(ValueError):
35+
"""Raised when OR groups contain nested OR conditions."""
36+
37+
def __init__(self):
38+
super().__init__("OR groups cannot contain nested OR conditions")
39+
40+
41+
class TooManyConditionsError(ValueError):
42+
"""Raised when the number of conditions exceeds the maximum allowed."""
43+
44+
def __init__(self, count: int, max_conditions: int):
45+
super().__init__(
46+
f"{max_conditions} conditions is the maximum, but {count} conditions are defined"
47+
)
48+
49+
50+
class InvalidParameterTypeError(TypeError):
51+
"""Raised when the type of the parameter is invalid."""
52+
53+
def __init__(self, parameter: Any, suggestion: str | None = None):
54+
message = f"Invalid Parameter type: {type(parameter)}"
55+
if suggestion:
56+
message += f". {suggestion}"
57+
super().__init__(message)
58+
59+
2760
class Operator(str, Enum):
2861
EQUAL = "=="
2962
NOT_EQUAL = "!="
@@ -45,6 +78,9 @@ def __str__(self) -> str:
4578
class Column(BaseModel):
4679
name: str
4780

81+
def __init__(self, name: str):
82+
super().__init__(name=name)
83+
4884
@override
4985
def __hash__(self) -> int:
5086
return hash(self.name)
@@ -107,22 +143,12 @@ def dict(self, **kwargs) -> dict:
107143
return result
108144

109145
def __and__(self, other: Condition) -> AndCondition:
110-
new_conditions = []
111-
for cond in [self, other]:
112-
if isinstance(cond, AndCondition):
113-
new_conditions.extend(cond.conditions)
114-
else:
115-
new_conditions.append(cond)
116-
return AndCondition(conditions=new_conditions)
146+
"""Construct an AndCondition"""
147+
return AndCondition(conditions=[self, other])
117148

118149
def __or__(self, other: Condition) -> OrCondition:
119-
new_conditions = []
120-
for cond in [self, other]:
121-
if isinstance(cond, OrCondition):
122-
new_conditions.extend(cond.conditions)
123-
else:
124-
new_conditions.append(cond)
125-
return OrCondition(conditions=new_conditions)
150+
"""Construct an OrCondition"""
151+
return OrCondition(conditions=[self, other])
126152

127153

128154
class NullityCondition(Condition):
@@ -140,7 +166,21 @@ class ComparisonCondition(Condition):
140166
type: Literal["comparison"] = Field(default="comparison")
141167
column: Column
142168
operator: Operator
143-
parameter: Parameter
169+
parameter: Parameter = Field(...)
170+
171+
@root_validator
172+
def _validate_parameter_not_none(cls, values):
173+
parameter = values.get("parameter")
174+
operator = values.get("operator")
175+
if parameter is None:
176+
if operator == Operator.EQUAL:
177+
suggestion = "Did you mean to use Column.is_null()?"
178+
elif operator == Operator.NOT_EQUAL:
179+
suggestion = "Did you mean to use Column.is_not_null()?"
180+
else:
181+
suggestion = None
182+
raise InvalidParameterTypeError(parameter, suggestion)
183+
return values
144184

145185
@override
146186
def __repr__(self):
@@ -235,3 +275,123 @@ def __repr__(self) -> str:
235275
PassThroughCondition,
236276
None,
237277
]
278+
279+
280+
# Maximum number of conditions allowed in a row_condition
281+
MAX_CONDITIONS = 100
282+
283+
284+
def _count_total_conditions(conditions: List[Condition]) -> int:
285+
"""Recursively count all conditions including nested ones."""
286+
count = 0
287+
for condition in conditions:
288+
if isinstance(condition, (AndCondition, OrCondition)):
289+
count += _count_total_conditions(condition.conditions)
290+
else:
291+
count += 1
292+
return count
293+
294+
295+
def _flatten_and_conditions(conditions: List[Condition]) -> List[Condition]:
296+
"""Flatten nested AndConditions within a list of conditions."""
297+
flattened = []
298+
for condition in conditions:
299+
if isinstance(condition, AndCondition):
300+
# Recursively flatten nested AndConditions
301+
flattened.extend(_flatten_and_conditions(condition.conditions))
302+
else:
303+
flattened.append(condition)
304+
return flattened
305+
306+
307+
def _contains_or_in_and(condition: Condition) -> bool:
308+
"""Check if an AndCondition contains any OrConditions."""
309+
if isinstance(condition, AndCondition):
310+
for cond in condition.conditions:
311+
if isinstance(cond, OrCondition):
312+
return True
313+
# Recursively check nested AndConditions
314+
if isinstance(cond, AndCondition) and _contains_or_in_and(cond):
315+
return True
316+
return False
317+
318+
319+
def _contains_nested_or(condition: Condition) -> bool:
320+
"""Check if an OrCondition contains nested OrConditions."""
321+
if isinstance(condition, OrCondition):
322+
for cond in condition.conditions:
323+
if isinstance(cond, OrCondition):
324+
return True
325+
# Also check if any nested AndConditions contain OrConditions
326+
# (which would create a nested OR structure)
327+
if isinstance(cond, AndCondition):
328+
for and_cond in cond.conditions:
329+
if isinstance(and_cond, OrCondition):
330+
# This is an OR within an AND within an OR, which creates nested ORs
331+
return True
332+
return False
333+
334+
335+
def _validate_and_condition(row_condition: AndCondition) -> AndCondition:
336+
"""Validate AndCondition constraints."""
337+
# Check for OrConditions within AndConditions
338+
if _contains_or_in_and(row_condition):
339+
raise AndContainsOrError()
340+
341+
# Flatten nested AndConditions
342+
# We only flatten nested AndConditions because
343+
# logical ANDs are associative, while logical ORs are non-associative.
344+
flattened_conditions = _flatten_and_conditions(row_condition.conditions)
345+
346+
# Return flattened AndCondition
347+
return AndCondition(conditions=flattened_conditions)
348+
349+
350+
def _validate_or_condition(row_condition: OrCondition) -> OrCondition:
351+
"""Validate OrCondition constraints."""
352+
# Check for nested OrConditions
353+
if _contains_nested_or(row_condition):
354+
raise NestedOrError()
355+
356+
return row_condition
357+
358+
359+
def validate_row_condition(row_condition: RowConditionType) -> RowConditionType:
360+
"""Validate row_condition according to GX Cloud UI constraints.
361+
362+
1. Flatten nested AndConditions within AndConditions
363+
2. Raise error if OrConditions are nested within AndConditions
364+
3. Raise error if OrConditions are nested within OrConditions
365+
4. Raise error if total conditions exceed MAX_CONDITIONS
366+
367+
Args:
368+
row_condition: The row condition to validate
369+
370+
Returns:
371+
The validated (and possibly flattened) row condition
372+
373+
Raises:
374+
ValueError: If the row condition violates any constraints
375+
"""
376+
# String conditions and None are always valid
377+
if row_condition is None or isinstance(row_condition, str):
378+
return row_condition
379+
380+
# Single condition types are always valid
381+
if isinstance(row_condition, (ComparisonCondition, NullityCondition, PassThroughCondition)):
382+
return row_condition
383+
384+
# Validate total condition count
385+
total_count = _count_total_conditions(row_condition.conditions)
386+
if total_count > MAX_CONDITIONS:
387+
raise TooManyConditionsError(total_count, MAX_CONDITIONS)
388+
389+
# Validate AndCondition
390+
if isinstance(row_condition, AndCondition):
391+
return _validate_and_condition(row_condition)
392+
393+
# Validate OrCondition
394+
if isinstance(row_condition, OrCondition):
395+
return _validate_or_condition(row_condition)
396+
397+
raise InvalidConditionTypeError(row_condition)

0 commit comments

Comments
 (0)