Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
78d0701
Update asset list_
alexluck-sift Sep 10, 2025
dbb026d
update asset get
alexluck-sift Sep 10, 2025
d405af0
update run list_
alexluck-sift Sep 11, 2025
608b866
Updates to run create to use pydantic model
alexluck-sift Sep 11, 2025
c609004
wip cleanup
alexluck-sift Sep 11, 2025
6bad3b8
update cel tests
alexluck-sift Sep 11, 2025
1d39779
runs cleanup
alexluck-sift Sep 11, 2025
a930f27
update calculated_channels
alexluck-sift Sep 11, 2025
9b48e1d
linting
alexluck-sift Sep 11, 2025
666b001
cleanup pydantic models to use composition
alexluck-sift Sep 12, 2025
e064a31
pydantic model cleanup
alexluck-sift Sep 12, 2025
cf5eacc
updated to CONTRIBUTING.md and moved timestamp.py to _internal
alexluck-sift Sep 12, 2025
c9f184d
clean up gen_pyi indentation
alexluck-sift Sep 13, 2025
fcf8234
add missing init
alexluck-sift Sep 13, 2025
22fd813
update tests
alexluck-sift Sep 15, 2025
9d2f268
wip commit
alexluck-sift Sep 26, 2025
ca375e4
fmt and update docs dependencies
alexluck-sift Sep 29, 2025
31eb871
linting
alexluck-sift Sep 29, 2025
aa5a95b
add duration to cel_utils.py
alexluck-sift Oct 6, 2025
3862419
update sift types with updated protos
alexluck-sift Oct 6, 2025
0a3a70f
update filters
alexluck-sift Oct 6, 2025
5a527dd
update asset archive
alexluck-sift Oct 6, 2025
a46cc2a
update run archive/unarchive
alexluck-sift Oct 6, 2025
6aedd6b
update rules archive/unarchive
alexluck-sift Oct 7, 2025
4b1eb2c
update update classes
alexluck-sift Oct 7, 2025
de9221f
update calculated channel archive/unarchive
alexluck-sift Oct 7, 2025
0ad8966
update rules create pattern
alexluck-sift Oct 7, 2025
e2feb35
linting
alexluck-sift Oct 7, 2025
388e94f
update ingestion to use ChannelConfig instead of Channel
alexluck-sift Oct 7, 2025
396d656
update channelconfig validation
alexluck-sift Oct 7, 2025
9545349
add timedelta cel test
alexluck-sift Oct 7, 2025
a073629
add missing is_adhoc from proto
alexluck-sift Oct 7, 2025
a2127f7
update tests and examples
alexluck-sift Oct 7, 2025
e816c6c
update types and rules list
alexluck-sift Oct 7, 2025
d715d5d
update pre-push-hook and update stubs
alexluck-sift Oct 7, 2025
292fb4f
update pre-push hooks
alexluck-sift Oct 8, 2025
a135e04
update proto in base type _update method
alexluck-sift Oct 8, 2025
c5cd2e8
remove unused check.sh
alexluck-sift Oct 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 11 additions & 48 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -1,61 +1,24 @@
#!/usr/bin/env bash

# ensure generated python stubs are up-to-date, from sync clients and sift_stream_bindings

# Store the root directory of the repository
REPO_ROOT="$(git rev-parse --show-toplevel)"
PYTHON_DIR="$REPO_ROOT/python"
BINDINGS_DIR="$REPO_ROOT/rust/crates/sift_stream_bindings"
STUBS_DIR="$PYTHON_DIR/lib/sift_client/resources/sync_stubs"

# Function to check if generated stub files have changed
check_stub_changes() {
local target_path="$1"
local changed_files=$(git status --porcelain "$target_path" | grep -E '\.pyi$' || true)

if [ -n "$changed_files" ]; then
echo "ERROR: Generated python stubs are not up-to-date. Please commit the changed files:"
echo "$changed_files"
exit 1
fi
}

# Function to generate Python stubs
generate_python_stubs() {
echo "Generating Python stubs..."
cd "$PYTHON_DIR"

if [[ ! -d "$PYTHON_DIR/venv" ]]; then
echo "Running bootstrap script..."
bash ./scripts/dev bootstrap
fi

bash ./scripts/dev gen-stubs
check_stub_changes "$STUBS_DIR"
}
GITHOOKS_DIR="$REPO_ROOT/.githooks"

# Function to generate bindings stubs
generate_bindings_stubs() {
echo "Generating bindings stubs..."
cd "$BINDINGS_DIR"
cargo run --bin stub_gen

# The stub file is generated in the bindings directory
local stub_file="$BINDINGS_DIR/sift_stream_bindings.pyi"
check_stub_changes "$stub_file"
}

# Check for changes in relevant files
# Check for changes in Python files
python_changed_files=($(git diff --name-only --diff-filter=ACM | grep '^python/lib/sift_client/' || true))
bindings_changed_files=($(git diff --name-only --diff-filter=ACM | grep '^rust/crates/sift_stream_bindings/src/' || true))

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

# Check for changes in Rust bindings files
bindings_changed_files=($(git diff --name-only --diff-filter=ACM | grep '^rust/crates/sift_stream_bindings/src/' || true))

if [[ -n "$bindings_changed_files" ]]; then
generate_bindings_stubs
echo "Rust bindings files changed, running Rust stub checks..."
bash "$GITHOOKS_DIR/pre-push-rust/stubs.sh"
fi

echo "All stubs are up-to-date."
echo "Pre-push checks completed successfully."

36 changes: 36 additions & 0 deletions .githooks/pre-push-python/stubs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# ensure generated python stubs are up-to-date, from sync clients

# Store the root directory of the repository
REPO_ROOT="$(git rev-parse --show-toplevel)"
PYTHON_DIR="$REPO_ROOT/python"
STUBS_DIR="$PYTHON_DIR/lib/sift_client/resources/sync_stubs"

# Function to check if generated stub files have changed
check_stub_changes() {
local target_path="$1"
local changed_files=$(git status --porcelain "$target_path" | grep -E '\.pyi$' || true)

if [ -n "$changed_files" ]; then
echo "ERROR: Generated python stubs are not up-to-date. Please commit the changed files:"
echo "$changed_files"
exit 1
fi
}

# Function to generate Python stubs
generate_python_stubs() {
echo "Generating Python stubs..."
cd "$PYTHON_DIR"

if [[ ! -d "$PYTHON_DIR/venv" ]]; then
echo "Running bootstrap script..."
bash ./scripts/dev bootstrap
fi

bash ./scripts/dev gen-stubs
check_stub_changes "$STUBS_DIR"
}

generate_python_stubs

echo "All stubs are up-to-date."
32 changes: 32 additions & 0 deletions .githooks/pre-push-rust/stubs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# ensure generated python stubs are up-to-date, from sift_stream_bindings

# Store the root directory of the repository
REPO_ROOT="$(git rev-parse --show-toplevel)"
BINDINGS_DIR="$REPO_ROOT/rust/crates/sift_stream_bindings"

# Function to check if generated stub files have changed
check_stub_changes() {
local target_path="$1"
local changed_files=$(git status --porcelain "$target_path" | grep -E '\.pyi$' || true)

if [ -n "$changed_files" ]; then
echo "ERROR: Generated python stubs are not up-to-date. Please commit the changed files:"
echo "$changed_files"
exit 1
fi
}

# Function to generate bindings stubs
generate_bindings_stubs() {
echo "Generating bindings stubs..."
cd "$BINDINGS_DIR"
cargo run --bin stub_gen

# The stub file is generated in the bindings directory
local stub_file="$BINDINGS_DIR/sift_stream_bindings.pyi"
check_stub_changes "$stub_file"
}

generate_bindings_stubs

echo "All stubs are up-to-date."
1 change: 1 addition & 0 deletions python/lib/sift_client/.ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ ignore = ["W191", "D206", "D300", # https://docs.astral.sh/ruff/formatter/#confl
"D105", # Missing docstring in magic method
"D205", # 1 blank line required between summary line and description
"D100", # Missing docstring in public module
"C408", # Allow dict()
]


Expand Down
98 changes: 95 additions & 3 deletions python/lib/sift_client/_internal/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,48 @@ All low-level clients should implement `LowLevelClientBase` from `sift_client/_i

### Sift Types

New Sift types can be implemented in `sift_client/types`.
New Sift types can be implemented in `sift_client/sift_types`.

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

All Sift types should inherit from `BaseType` and model updates from `ModelUpdate` in `sift_client/types/_base.py`
All Sift types should inherit from `BaseType` in `sift_client/sift_types/_base.py`

#### Create/Update Pydantic Model Inheritance Pattern

The Sift client uses a composition-based inheritance pattern for Pydantic models to avoid complex multiple inheritance issues:

1. **Base Classes** (`sift_client/sift_types/_base.py`):
- `ModelCreateUpdateBase`: Base class containing shared functionality for proto conversion and field mapping
- `ModelCreate`: Inherits from `ModelCreateUpdateBase` with generic typing for creation operations
- `ModelUpdate`: Inherits from `ModelCreateUpdateBase` with additional field mask support for updates

2. **Domain-Specific Base Classes**:
Create a base class that inherits from `ModelCreateUpdateBase` and contains:
- All shared field definitions
- Shared `_to_proto_helpers` configuration for complex proto mappings
- Common validation logic using `@model_validator`
It may not always make sense to implement a base class if there is little/no overlap in fields or protos.

3. **Create and Update Models**:
- `{Domain}Create`: Inherits from both `{Domain}Base` and `ModelCreate[{CreateProto}]`
- Include create only fields and validators
- `{Domain}Update`: Inherits from both `{Domain}Base` and `ModelUpdate[{UpdateProto}]`
- Include update only fields and validators

#### Proto Mapping Helpers

Use `MappingHelper` for complex proto field mappings when the Pydantic model doesn't match the proto model exactly:
- `proto_attr_path`: Dot-separated path to the proto field
- `update_field`: Field name for update masks (optional)
- `converter`: Function/class to convert the value (optional)

#### Validation Guidelines

- Use `@model_validator(mode="after")` for cross-field validation
- Prefix validation method names with `_` (e.g., `_validate_time_fields`) since these don't need to be user visible
- Keep validation logic in the base class when shared between create/update
- Add specific validation in create/update classes as needed

### High-Level APIs

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

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

#### Resource Method Patterns

Resource classes should implement consistent patterns for common operations. Use the helper methods from `ResourceBase` to build standard filter arguments.

**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.


**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.

Examples:
```python
# Accept either Asset object or asset ID string
async def update(self, asset: Asset | str, ...) -> Asset:
```

##### Standard Method Signatures

**`get(resource_id: str) -> {Type}`**
- Single required positional argument for the resource ID
- Returns the specific resource instance

**`list_(...) -> list[{Type}]`**
- Use `list_` (with underscore) to avoid conflicts with Python's built-in `list`
- Standard filter arguments in consistent order (as applicable:)
1. Name filters: `name`, `name_contains`, `name_regex`
2. Self IDs: Resource-specific ID filters (e.g., `run_ids`, `asset_ids`, `client_keys`)
3. Created/modified ranges: `created_after`, `created_before`, `modified_after`, `modified_before`
4. Created/modified users: `created_by`, `modified_by`
5. Metadata: `metadata`, `tags`
6. Resource-specific filters: Domain-specific filters (e.g., `assets`, `duration_less_than`, `start_time_after`)
7. Common filters: `description_contains`, `include_archived`, `filter_query`
8. Ordering and pagination: `order_by`, `limit`, `page_size`, `page_token`

**`find(...) -> {Type} | None`**
- Similar signature to `list_` but returns single result or None
- Should use the same filter arguments as `list_`

**`create(create: {Type}Create | dict, **kwargs) -> {Type}`**
- Accept both Pydantic model and dict
- Additional keyword arguments for operation-specific options

**`update({resource}: str | {Type}, update: {Type}Update | dict, **kwargs) -> {Type}`**
- First argument accepts either ID string or resource instance
- Update model as second argument
- Additional keyword arguments for operation-specific options

##### Using ResourceBase Helper Methods

The `ResourceBase` class provides helper methods to build consistent CEL filter expressions:

- `_build_name_cel_filters()`: Handles `name`, `name_contains`, `name_regex`
- `_build_time_cel_filters()`: Handles time-based filters and user filters
- `_build_tags_metadata_cel_filters()`: Handles `tags` and `metadata` filters
- `_build_common_cel_filters()`: Handles `description_contains`, `include_archived`, `filter_query`


#### Sync API Generation

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
Expand Down
4 changes: 3 additions & 1 deletion python/lib/sift_client/_internal/gen_pyi.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ def generate_stubs_for_module(path_arg: str | pathlib.Path) -> dict[pathlib.Path
raw_doc = inspect.getdoc(cls) or ""
if raw_doc:
doc = (
' """\n' + "\n".join(f" {l}" for l in raw_doc.splitlines()) + '\n """'
' """\n'
+ "\n".join(f" {l.strip()}" for l in raw_doc.splitlines())
+ '\n """'
)
else:
doc = " ..."
Expand Down
11 changes: 7 additions & 4 deletions python/lib/sift_client/_internal/low_level_wrappers/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from typing import Any, cast

from sift.assets.v1.assets_pb2 import (
DeleteAssetRequest,
ArchiveAssetRequest,
ArchiveAssetResponse,
GetAssetRequest,
GetAssetResponse,
ListAssetsRequest,
Expand Down Expand Up @@ -96,6 +97,8 @@ async def update_asset(self, update: AssetUpdate) -> Asset:
updated_grpc_asset = cast("UpdateAssetResponse", response).asset
return Asset._from_proto(updated_grpc_asset)

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