Skip to content

Commit cb2c9c1

Browse files
python(feat): sift_client consistency (#312)
1 parent 8e0a0e0 commit cb2c9c1

40 files changed

Lines changed: 1798 additions & 1505 deletions

.githooks/pre-push

Lines changed: 11 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,24 @@
11
#!/usr/bin/env bash
22

3-
# ensure generated python stubs are up-to-date, from sync clients and sift_stream_bindings
4-
53
# Store the root directory of the repository
64
REPO_ROOT="$(git rev-parse --show-toplevel)"
7-
PYTHON_DIR="$REPO_ROOT/python"
8-
BINDINGS_DIR="$REPO_ROOT/rust/crates/sift_stream_bindings"
9-
STUBS_DIR="$PYTHON_DIR/lib/sift_client/resources/sync_stubs"
10-
11-
# Function to check if generated stub files have changed
12-
check_stub_changes() {
13-
local target_path="$1"
14-
local changed_files=$(git status --porcelain "$target_path" | grep -E '\.pyi$' || true)
15-
16-
if [ -n "$changed_files" ]; then
17-
echo "ERROR: Generated python stubs are not up-to-date. Please commit the changed files:"
18-
echo "$changed_files"
19-
exit 1
20-
fi
21-
}
22-
23-
# Function to generate Python stubs
24-
generate_python_stubs() {
25-
echo "Generating Python stubs..."
26-
cd "$PYTHON_DIR"
27-
28-
if [[ ! -d "$PYTHON_DIR/venv" ]]; then
29-
echo "Running bootstrap script..."
30-
bash ./scripts/dev bootstrap
31-
fi
32-
33-
bash ./scripts/dev gen-stubs
34-
check_stub_changes "$STUBS_DIR"
35-
}
5+
GITHOOKS_DIR="$REPO_ROOT/.githooks"
366

37-
# Function to generate bindings stubs
38-
generate_bindings_stubs() {
39-
echo "Generating bindings stubs..."
40-
cd "$BINDINGS_DIR"
41-
cargo run --bin stub_gen
42-
43-
# The stub file is generated in the bindings directory
44-
local stub_file="$BINDINGS_DIR/sift_stream_bindings.pyi"
45-
check_stub_changes "$stub_file"
46-
}
47-
48-
# Check for changes in relevant files
7+
# Check for changes in Python files
498
python_changed_files=($(git diff --name-only --diff-filter=ACM | grep '^python/lib/sift_client/' || true))
50-
bindings_changed_files=($(git diff --name-only --diff-filter=ACM | grep '^rust/crates/sift_stream_bindings/src/' || true))
519

52-
# Generate stubs if needed
5310
if [[ -n "$python_changed_files" ]]; then
54-
generate_python_stubs
11+
echo "Python files changed, running Python stub checks..."
12+
bash "$GITHOOKS_DIR/pre-push-python/stubs.sh"
5513
fi
5614

15+
# Check for changes in Rust bindings files
16+
bindings_changed_files=($(git diff --name-only --diff-filter=ACM | grep '^rust/crates/sift_stream_bindings/src/' || true))
17+
5718
if [[ -n "$bindings_changed_files" ]]; then
58-
generate_bindings_stubs
19+
echo "Rust bindings files changed, running Rust stub checks..."
20+
bash "$GITHOOKS_DIR/pre-push-rust/stubs.sh"
5921
fi
6022

61-
echo "All stubs are up-to-date."
23+
echo "Pre-push checks completed successfully."
24+

.githooks/pre-push-python/stubs.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# ensure generated python stubs are up-to-date, from sync clients
2+
3+
# Store the root directory of the repository
4+
REPO_ROOT="$(git rev-parse --show-toplevel)"
5+
PYTHON_DIR="$REPO_ROOT/python"
6+
STUBS_DIR="$PYTHON_DIR/lib/sift_client/resources/sync_stubs"
7+
8+
# Function to check if generated stub files have changed
9+
check_stub_changes() {
10+
local target_path="$1"
11+
local changed_files=$(git status --porcelain "$target_path" | grep -E '\.pyi$' || true)
12+
13+
if [ -n "$changed_files" ]; then
14+
echo "ERROR: Generated python stubs are not up-to-date. Please commit the changed files:"
15+
echo "$changed_files"
16+
exit 1
17+
fi
18+
}
19+
20+
# Function to generate Python stubs
21+
generate_python_stubs() {
22+
echo "Generating Python stubs..."
23+
cd "$PYTHON_DIR"
24+
25+
if [[ ! -d "$PYTHON_DIR/venv" ]]; then
26+
echo "Running bootstrap script..."
27+
bash ./scripts/dev bootstrap
28+
fi
29+
30+
bash ./scripts/dev gen-stubs
31+
check_stub_changes "$STUBS_DIR"
32+
}
33+
34+
generate_python_stubs
35+
36+
echo "All stubs are up-to-date."

.githooks/pre-push-rust/stubs.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# ensure generated python stubs are up-to-date, from sift_stream_bindings
2+
3+
# Store the root directory of the repository
4+
REPO_ROOT="$(git rev-parse --show-toplevel)"
5+
BINDINGS_DIR="$REPO_ROOT/rust/crates/sift_stream_bindings"
6+
7+
# Function to check if generated stub files have changed
8+
check_stub_changes() {
9+
local target_path="$1"
10+
local changed_files=$(git status --porcelain "$target_path" | grep -E '\.pyi$' || true)
11+
12+
if [ -n "$changed_files" ]; then
13+
echo "ERROR: Generated python stubs are not up-to-date. Please commit the changed files:"
14+
echo "$changed_files"
15+
exit 1
16+
fi
17+
}
18+
19+
# Function to generate bindings stubs
20+
generate_bindings_stubs() {
21+
echo "Generating bindings stubs..."
22+
cd "$BINDINGS_DIR"
23+
cargo run --bin stub_gen
24+
25+
# The stub file is generated in the bindings directory
26+
local stub_file="$BINDINGS_DIR/sift_stream_bindings.pyi"
27+
check_stub_changes "$stub_file"
28+
}
29+
30+
generate_bindings_stubs
31+
32+
echo "All stubs are up-to-date."

python/lib/sift_client/.ruff.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ ignore = ["W191", "D206", "D300", # https://docs.astral.sh/ruff/formatter/#confl
4949
"D105", # Missing docstring in magic method
5050
"D205", # 1 blank line required between summary line and description
5151
"D100", # Missing docstring in public module
52+
"C408", # Allow dict()
5253
]
5354

5455

python/lib/sift_client/_internal/CONTRIBUTING.md

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,48 @@ All low-level clients should implement `LowLevelClientBase` from `sift_client/_i
4646

4747
### Sift Types
4848

49-
New Sift types can be implemented in `sift_client/types`.
49+
New Sift types can be implemented in `sift_client/sift_types`.
5050

5151
These types are used to define Pydantic models for all domain objects and to convert between protocol buffers and Python. Additional
52-
update models can be implemented for performing updates with field masks.
52+
update and create models can be implemented for performing updates with field masks.
5353

54-
All Sift types should inherit from `BaseType` and model updates from `ModelUpdate` in `sift_client/types/_base.py`
54+
All Sift types should inherit from `BaseType` in `sift_client/sift_types/_base.py`
55+
56+
#### Create/Update Pydantic Model Inheritance Pattern
57+
58+
The Sift client uses a composition-based inheritance pattern for Pydantic models to avoid complex multiple inheritance issues:
59+
60+
1. **Base Classes** (`sift_client/sift_types/_base.py`):
61+
- `ModelCreateUpdateBase`: Base class containing shared functionality for proto conversion and field mapping
62+
- `ModelCreate`: Inherits from `ModelCreateUpdateBase` with generic typing for creation operations
63+
- `ModelUpdate`: Inherits from `ModelCreateUpdateBase` with additional field mask support for updates
64+
65+
2. **Domain-Specific Base Classes**:
66+
Create a base class that inherits from `ModelCreateUpdateBase` and contains:
67+
- All shared field definitions
68+
- Shared `_to_proto_helpers` configuration for complex proto mappings
69+
- Common validation logic using `@model_validator`
70+
It may not always make sense to implement a base class if there is little/no overlap in fields or protos.
71+
72+
3. **Create and Update Models**:
73+
- `{Domain}Create`: Inherits from both `{Domain}Base` and `ModelCreate[{CreateProto}]`
74+
- Include create only fields and validators
75+
- `{Domain}Update`: Inherits from both `{Domain}Base` and `ModelUpdate[{UpdateProto}]`
76+
- Include update only fields and validators
77+
78+
#### Proto Mapping Helpers
79+
80+
Use `MappingHelper` for complex proto field mappings when the Pydantic model doesn't match the proto model exactly:
81+
- `proto_attr_path`: Dot-separated path to the proto field
82+
- `update_field`: Field name for update masks (optional)
83+
- `converter`: Function/class to convert the value (optional)
84+
85+
#### Validation Guidelines
86+
87+
- Use `@model_validator(mode="after")` for cross-field validation
88+
- Prefix validation method names with `_` (e.g., `_validate_time_fields`) since these don't need to be user visible
89+
- Keep validation logic in the base class when shared between create/update
90+
- Add specific validation in create/update classes as needed
5591

5692
### High-Level APIs
5793

@@ -62,6 +98,62 @@ Static and class methods should be avoided since these cannot have associated sy
6298

6399
All high-level APIs should inherit from `ResourceBase` from `sift_client/resources/_base.py`.
64100

101+
#### Resource Method Patterns
102+
103+
Resource classes should implement consistent patterns for common operations. Use the helper methods from `ResourceBase` to build standard filter arguments.
104+
105+
**Important:** Arguments that represent another Sift Type should always accept both the object instance and its ID string. This provides flexibility for users who may have either form.
106+
107+
108+
**Note**: If the proto API does not support filters for a resource, the API should be updated to make the resource filterable in a consistent way with other resources.
109+
110+
Examples:
111+
```python
112+
# Accept either Asset object or asset ID string
113+
async def update(self, asset: Asset | str, ...) -> Asset:
114+
```
115+
116+
##### Standard Method Signatures
117+
118+
**`get(resource_id: str) -> {Type}`**
119+
- Single required positional argument for the resource ID
120+
- Returns the specific resource instance
121+
122+
**`list_(...) -> list[{Type}]`**
123+
- Use `list_` (with underscore) to avoid conflicts with Python's built-in `list`
124+
- Standard filter arguments in consistent order (as applicable:)
125+
1. Name filters: `name`, `name_contains`, `name_regex`
126+
2. Self IDs: Resource-specific ID filters (e.g., `run_ids`, `asset_ids`, `client_keys`)
127+
3. Created/modified ranges: `created_after`, `created_before`, `modified_after`, `modified_before`
128+
4. Created/modified users: `created_by`, `modified_by`
129+
5. Metadata: `metadata`, `tags`
130+
6. Resource-specific filters: Domain-specific filters (e.g., `assets`, `duration_less_than`, `start_time_after`)
131+
7. Common filters: `description_contains`, `include_archived`, `filter_query`
132+
8. Ordering and pagination: `order_by`, `limit`, `page_size`, `page_token`
133+
134+
**`find(...) -> {Type} | None`**
135+
- Similar signature to `list_` but returns single result or None
136+
- Should use the same filter arguments as `list_`
137+
138+
**`create(create: {Type}Create | dict, **kwargs) -> {Type}`**
139+
- Accept both Pydantic model and dict
140+
- Additional keyword arguments for operation-specific options
141+
142+
**`update({resource}: str | {Type}, update: {Type}Update | dict, **kwargs) -> {Type}`**
143+
- First argument accepts either ID string or resource instance
144+
- Update model as second argument
145+
- Additional keyword arguments for operation-specific options
146+
147+
##### Using ResourceBase Helper Methods
148+
149+
The `ResourceBase` class provides helper methods to build consistent CEL filter expressions:
150+
151+
- `_build_name_cel_filters()`: Handles `name`, `name_contains`, `name_regex`
152+
- `_build_time_cel_filters()`: Handles time-based filters and user filters
153+
- `_build_tags_metadata_cel_filters()`: Handles `tags` and `metadata` filters
154+
- `_build_common_cel_filters()`: Handles `description_contains`, `include_archived`, `filter_query`
155+
156+
65157
#### Sync API Generation
66158

67159
To generate a sync API from an async API, add a `generate_sync_api` function call in `sift_client/resources/sync_stubs/__init__.py` and

python/lib/sift_client/_internal/gen_pyi.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,9 @@ def generate_stubs_for_module(path_arg: str | pathlib.Path) -> dict[pathlib.Path
143143
raw_doc = inspect.getdoc(cls) or ""
144144
if raw_doc:
145145
doc = (
146-
' """\n' + "\n".join(f" {l}" for l in raw_doc.splitlines()) + '\n """'
146+
' """\n'
147+
+ "\n".join(f" {l.strip()}" for l in raw_doc.splitlines())
148+
+ '\n """'
147149
)
148150
else:
149151
doc = " ..."

python/lib/sift_client/_internal/low_level_wrappers/assets.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from typing import Any, cast
44

55
from sift.assets.v1.assets_pb2 import (
6-
DeleteAssetRequest,
6+
ArchiveAssetRequest,
7+
ArchiveAssetResponse,
78
GetAssetRequest,
89
GetAssetResponse,
910
ListAssetsRequest,
@@ -96,6 +97,8 @@ async def update_asset(self, update: AssetUpdate) -> Asset:
9697
updated_grpc_asset = cast("UpdateAssetResponse", response).asset
9798
return Asset._from_proto(updated_grpc_asset)
9899

99-
async def delete_asset(self, asset_id: str, archive_runs: bool = False) -> None:
100-
request = DeleteAssetRequest(asset_id=asset_id, archive_runs=archive_runs)
101-
await self._grpc_client.get_stub(AssetServiceStub).DeleteAsset(request)
100+
async def archive_asset(self, asset_id: str, archive_runs: bool = False) -> list[str] | None:
101+
request = ArchiveAssetRequest(asset_id=asset_id, archive_runs=archive_runs)
102+
response = await self._grpc_client.get_stub(AssetServiceStub).ArchiveAsset(request)
103+
response = cast("ArchiveAssetResponse", response)
104+
return response.archived_runs

0 commit comments

Comments
 (0)