diff --git a/.github/workflows/python_build_docs.yml b/.github/workflows/python_build_docs.yml new file mode 100644 index 000000000..912cb3d33 --- /dev/null +++ b/.github/workflows/python_build_docs.yml @@ -0,0 +1,118 @@ +name: Build and Deploy Python Docs (Dev) + +on: + pull_request: + types: [opened, synchronize, closed] + paths: + - 'python/docs/**' + - 'python/lib/**' + - 'python/mkdocs.yml' + - 'python/pyproject.toml' + workflow_dispatch: + inputs: + deploy_to_dev: + description: 'Deploy to dev alias using commit hash as version' + required: false + default: true + type: boolean + +jobs: + build_docs: + if: github.event.action != 'closed' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + cd python + pip install -e .[docs,development] + + - name: Extract version + id: version + run: | + # Use commit hash as version for dev deployments + VERSION=$(git rev-parse --short HEAD) + + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + # Use PR number as alias for PR deployments + ALIAS="pr-${{ github.event.number }}" + else + # Use 'dev' for manual workflow dispatch + ALIAS="dev" + fi + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "alias=$ALIAS" >> $GITHUB_OUTPUT + echo "Dev deployment - Version: $VERSION, Alias: $ALIAS" + + - name: Fetch gh-pages branch + run: git fetch origin gh-pages --depth=1 + + - name: Configure Git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Deploy docs with mike + run: | + cd python + # Deploy dev/PR docs with hidden property to hide from version dropdown + mike deploy ${{ steps.version.outputs.alias }} --push --update-aliases --prop-set hidden=true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + cleanup_docs: + if: github.event.action == 'closed' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + cd python + pip install -e .[docs,development] + + - name: Fetch gh-pages branch + run: git fetch origin gh-pages --depth=1 + + - name: Configure Git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Delete PR docs + run: | + cd python + PR_ALIAS="pr-${{ github.event.number }}" + echo "Deleting docs for: $PR_ALIAS" + + # Check if the PR docs exist before trying to delete + if mike list | grep -q "$PR_ALIAS"; then + mike delete "$PR_ALIAS" --push + echo "Successfully deleted docs for $PR_ALIAS" + else + echo "No docs found for $PR_ALIAS, nothing to delete" + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/python_release.yaml b/.github/workflows/python_release.yaml index 3c8be0286..7c1e855b0 100644 --- a/.github/workflows/python_release.yaml +++ b/.github/workflows/python_release.yaml @@ -87,4 +87,89 @@ jobs: echo "Uploading archive: $archive" gh release upload "$TAG_NAME" "$archive" --clobber done + + deploy-docs: + name: Deploy Documentation + needs: publish-to-pypi + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + cd python + pip install -e .[docs,development] + + - name: Extract version and check if stable + id: version + run: | + # Extract version from tag (everything after 'python/') + FULL_VERSION=${GITHUB_REF#refs/tags/python/} + echo "Full version from tag: $FULL_VERSION" + + # Extract major.minor from version (drop patch) + if [[ "$FULL_VERSION" =~ ^v?([0-9]+)\.([0-9]+)\.[0-9]+(.*)?$ ]]; then + MAJOR=${BASH_REMATCH[1]} + MINOR=${BASH_REMATCH[2]} + SUFFIX=${BASH_REMATCH[3]} + VERSION="v${MAJOR}.${MINOR}" + + # Check if this is a stable release (no suffix like -alpha, -beta, -rc) + if [[ -z "$SUFFIX" ]]; then + # Stable release - use 'latest' alias and make visible + ALIAS="latest" + HIDDEN="false" + echo "Stable release detected: $FULL_VERSION -> $VERSION" + else + # Pre-release (alpha, beta, rc) - no 'latest' alias and hide from dropdown + VERSION="${VERSION}${SUFFIX}" + ALIAS="" + HIDDEN="true" + echo "Pre-release detected: $FULL_VERSION -> $VERSION" + fi + else + echo "Error: Could not parse version format: $FULL_VERSION" + exit 1 + fi + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "alias=$ALIAS" >> $GITHUB_OUTPUT + echo "hidden=$HIDDEN" >> $GITHUB_OUTPUT + echo "Final - Version: $VERSION, Alias: $ALIAS, Hidden: $HIDDEN" + + - name: Fetch gh-pages branch + run: git fetch origin gh-pages --depth=1 + + - name: Configure Git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Deploy docs with mike + run: | + cd python + VERSION="${{ steps.version.outputs.version }}" + ALIAS="${{ steps.version.outputs.alias }}" + HIDDEN="${{ steps.version.outputs.hidden }}" + # Always deploy the full version first, but hide it + echo "Deploying full version $FULL_VERSION (hidden)" + mike deploy "$FULL_VERSION" --push --prop-set hidden=true + + if [[ "$HIDDEN" == "false" ]]; then + # Stable release: deploy abbreviated version with latest alias, visible in dropdown + echo "Deploying stable release $VERSION with $ALIAS alias" + mike deploy "$VERSION" "$ALIAS" --push --update-aliases + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/python/docs/index.md b/python/docs/index.md new file mode 100644 index 000000000..a177f1054 --- /dev/null +++ b/python/docs/index.md @@ -0,0 +1,68 @@ +# Sift Python Client Library +Welcome to the official Python client library for Sift! This library provides a high-level Python API on top of Sift's protocol buffers, designed to ergonomically interface with the Sift gRPC API and simplify the process of streaming data. + +Sift provides official client libraries for select languages, designed to simplify the process of streaming data over gRPC. These client libraries utilize ingestion-config-based streaming to facilitate data transmission. + +Check out the [repository](https://github.com/sift-stack/sift) for a list of all available client libraries. + +## Installation + +To install the Sift Python library: + +```bash +pip install sift-stack-py +``` + +## API Documentation + +This documentation covers two Python APIs for interacting with Sift: + +### Sift Py API + +The original low-level Python API that provides direct access to Sift's protocol buffer interfaces. + +Browse the [**Sift Py API**][sift_py] section for complete reference documentation. + +**Use this API if you need:** + +- Direct protocol buffer access +- Fine-grained control over gRPC connections +- Legacy compatibility with existing code + +### Sift Client API (New) + +!!! warning + The Sift Client is experimental and is subject to change. + + +The modern, high-level client library that provides all the ergonomic features missing from the original API. This new client offers intuitive Python interfaces, strong type safety, automatic connection management, and both synchronous and asynchronous support. + +Explore the [**Sift Client API (New)**][sift_client] section for the complete API reference. + +**Key improvements over Sift Py:** + +- **Ergonomic Design** - Pythonic interfaces instead of raw protocol buffers +- **Type Safety** - Full type hints and Pydantic model validation +- **Dual APIs** - Both sync and async support for all operations +- **Auto Connection Management** - No manual gRPC connection handling +- **Rich Object Models** - Immutable types with convenient methods +- **Modern Patterns** - Context managers, iterators, and Python best practices + + +## Getting help + +- **API Reference** - Browse the complete API documentation in the navigation +- **Examples** - Check out code examples throughout the documentation +- **GitHub** - Visit our [repository](https://github.com/sift-stack/sift) for issues and contributions + +## What's next? + +Ready to dive deeper? Explore the API documentation to learn about: + +- **Sift Resources** - Creating, updating, and organizing your assets and other data +- **Data Streaming** - Efficient methods for ingesting data +- **Advanced Filtering** - Powerful query capabilities +- **Error Handling** - Best practices for robust applications +- **Performance Optimization** - Tips for high-throughput scenarios + +Get started by exploring the API reference in the navigation menu! diff --git a/python/docs/overrides/main.html b/python/docs/overrides/main.html new file mode 100644 index 000000000..648b7a6fe --- /dev/null +++ b/python/docs/overrides/main.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block outdated %} +You're not viewing the latest version. + + + + Click here to go to latest. + +{% endblock %} diff --git a/python/docs/overrides/partials/logo.html b/python/docs/overrides/partials/logo.html new file mode 100644 index 000000000..a7553e811 --- /dev/null +++ b/python/docs/overrides/partials/logo.html @@ -0,0 +1,2 @@ +logo +logo \ No newline at end of file diff --git a/python/docs/stylesheets/extra.css b/python/docs/stylesheets/extra.css new file mode 100644 index 000000000..92b39513c --- /dev/null +++ b/python/docs/stylesheets/extra.css @@ -0,0 +1,236 @@ +/* MkDocs Material Dark Theme Overrides - Fumadocs Style */ + +:root { + --tomato-1: #f9f6f5; + --tomato-2: #f9f2f0; + --tomato-3: #f9e4df; + --tomato-4: #ffd4ca; + --tomato-5: #fec4b7; + --tomato-6: #f9b3a4; + --tomato-7: #f19e8c; + --tomato-8: #e9826d; + --tomato-9: #ee4220; + --tomato-10: #e03006; + --tomato-11: #c82700; + --tomato-12: #5f2519; + --tomato-a1: #96612e05; + --tomato-a2: #c947140a; + --tomato-a3: #e933041b; + --tomato-a4: #ffd4ca; + --tomato-a5: #ffb3a2c0; + --tomato-a6: #f62f0358; + --tomato-a7: #e42a0170; + --tomato-a8: #dc270290; + --tomato-a9: #ed2801df; + --tomato-a10: #df2b00f9; + --tomato-a11: #c82700; + --tomato-a12: #4e0e01e6; + + --brand-color: var(--tomato-9); + + /* MkDocs Material Accent Colors */ + --md-accent-fg-color: var(--brand-color); + --md-accent-fg-color--transparent: rgb(from var(--brand-color) r g b / 0.1); + --md-accent-bg-color: hsla(0, 0%, 100%, 1); + --md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7); +} + +/* ============================================================================= + LOGO DISPLAY CONTROLS + ============================================================================= */ + +#logo_light_mode { + display: var(--md-footer-logo-light-mode); +} + +#logo_dark_mode { + display: var(--md-footer-logo-dark-mode); +} + +/* ============================================================================= + DARK THEME VARIABLES + ============================================================================= */ + +[data-md-color-scheme="slate"] { + /* Primary Colors */ + --md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7); + --md-default-bg-color: rgb(0, 0, 0); + --md-primary-bg-color: var(--font-foreground-color); + + /* Logo Display */ + --md-footer-logo-dark-mode: block; + --md-footer-logo-light-mode: none; + + /* Custom Theme Colors */ + --header-bg-color: hsl(0 0% 8.04%/.8); + --bold-header-color: white; + --font-foreground-color: rgb(255, 255, 255); + --border-color: rgb(36, 36, 36); + --code-box-background-color: rgb(18, 18, 18); + --search-bg: rgba(237, 237, 237, 0); + + /* Link Colors */ + --md-typeset-a-color: var(--brand-color) !important; +} + +/* ============================================================================= + LIGHT THEME VARIABLES + ============================================================================= */ + +[data-md-color-scheme="default"] { + /* Primary Colors */ + --md-primary-bg-color--light: hsla(0, 0%, 0%, 0.7); + --md-default-bg-color: rgb(255, 255, 255); + --md-primary-bg-color: var(--font-foreground-color); + + /* Logo Display */ + --md-footer-logo-dark-mode: none; + --md-footer-logo-light-mode: block; + + /* Custom Theme Colors */ + --header-bg-color: hsl(0 0% 96%/.8); + --bold-header-color: black; + --font-foreground-color: rgb(10, 10, 10); + --border-color: rgb(229, 229, 229); + --code-box-background-color: rgba(237, 237, 237, 0.5); + --search-bg: rgba(237, 237, 237, 0); + + /* Link Colors */ + --md-typeset-a-color: var(--brand-color) !important; +} + +/* ============================================================================= + HEADER STYLES + ============================================================================= */ + +.md-header, .md-tabs { + background-color: var(--header-bg-color) !important; + box-shadow: none !important; +} + +.md-header__title { + margin-left: 2px !important; +} + +.md-header__topic { + color: var(--bold-header-color) !important; +} + +.md-tabs, .md-header--shadow { + border-bottom: var(--border-color) solid 1px; +} + +/* ============================================================================= + LOGO STYLES + ============================================================================= */ + +.md-header__button.md-logo { + margin: 0 !important; + padding: 0 !important; + + > img { + height: 2rem !important; + } +} + +/* ============================================================================= + TYPOGRAPHY STYLES + ============================================================================= */ + +.md-typeset { + font-size: 16px !important; + font-weight: 300; + + h1 { + color: var(--font-foreground-color); + font-weight: 700; + } + + h2 { + color: var(--font-foreground-color); + font-weight: 600; + } + + h3 { + color: var(--font-foreground-color); + font-weight: 600; + } +} + +.md-typeset a:hover { + text-decoration: underline; +} + +/* ============================================================================= + NAVIGATION STYLES + ============================================================================= */ + +.md-nav__title { + color: var(--font-foreground-color); + background-color: var(--md-default-bg-color) !important; +} + +.md-nav__source { + background-color: var(--md-default-bg-color) !important; +} + +.md-nav__item { + font-weight: 300; +} + +.md-nav__item--active, .md-nav__link--active, .md-tabs__item--active { + font-weight: 600; + color: var(--font-foreground-color); +} + +/* ============================================================================= + SIDEBAR STYLES + ============================================================================= */ + +.md-sidebar--primary { + border-right: var(--border-color) solid 1px; + height: 100%; +} + +/* ============================================================================= + LAYOUT STYLES + ============================================================================= */ + +.md-main__inner { + margin-top: 0 !important; +} + +/* ============================================================================= + CODE BLOCK STYLES + ============================================================================= */ + +.md-code__content { + border-radius: 7px !important; + border: var(--border-color) solid 1px; + background-color: var(--code-box-background-color) !important; + font-size: 14px !important; +} + +/* ============================================================================= + SEARCH STYLES + ============================================================================= */ + +.md-search__input, .md-search__suggest, .md-search__form { + background-color: var(--search-bg) !important; + border-radius: 0.5rem !important; + border: var(--border-color) solid 1px; + + ::placeholder { + color: var(--font-foreground-color) !important; + font-weight: 300; + } +} + +/* ============================================================================= + BANNER STYLES + ============================================================================= */ + +.md-banner__inner { + margin-top: 0; + margin-bottom: 0; +} \ No newline at end of file diff --git a/python/docs/stylesheets/sift-favicon.ico b/python/docs/stylesheets/sift-favicon.ico new file mode 100644 index 000000000..47e2cd32e Binary files /dev/null and b/python/docs/stylesheets/sift-favicon.ico differ diff --git a/python/docs/stylesheets/sift_logo_dark.svg b/python/docs/stylesheets/sift_logo_dark.svg new file mode 100644 index 000000000..13596ce75 --- /dev/null +++ b/python/docs/stylesheets/sift_logo_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/python/docs/stylesheets/sift_logo_light.svg b/python/docs/stylesheets/sift_logo_light.svg new file mode 100644 index 000000000..75e3f8bdb --- /dev/null +++ b/python/docs/stylesheets/sift_logo_light.svg @@ -0,0 +1,3 @@ + + + diff --git a/python/griffe_extensions/__init__.py b/python/griffe_extensions/__init__.py new file mode 100644 index 000000000..8a5014afa --- /dev/null +++ b/python/griffe_extensions/__init__.py @@ -0,0 +1,3 @@ +from griffe_extensions.sync_stubs_inspector import InspectSpecificObjects + +__all__ = ["InspectSpecificObjects"] diff --git a/python/griffe_extensions/sync_stubs_inspector.py b/python/griffe_extensions/sync_stubs_inspector.py new file mode 100644 index 000000000..ec83e2600 --- /dev/null +++ b/python/griffe_extensions/sync_stubs_inspector.py @@ -0,0 +1,21 @@ +import griffe + +logger = griffe.get_logger("griffe_inspect_specific_objects") + + +class InspectSpecificObjects(griffe.Extension): + """Only inspect specific objects (such as ones with stubs)""" + + def __init__(self, paths: list[str]) -> None: + self.objects = paths + + def on_instance(self, *, obj: griffe.Object, **kwargs) -> None: + if obj.path not in self.objects: + return + + # Skip over the stub files themselves + if str(obj.filepath).endswith(".pyi"): + return + # Load the stub file instead of importing the .py + inspected_module = griffe.inspect(obj.module.path, filepath=obj.filepath) + obj.parent.set_member(obj.name, inspected_module[obj.name]) diff --git a/python/lib/sift_client/__init__.py b/python/lib/sift_client/__init__.py index 7589627f7..edbe5eaba 100644 --- a/python/lib/sift_client/__init__.py +++ b/python/lib/sift_client/__init__.py @@ -1,3 +1,208 @@ +""" +!!! warning + The Sift Client is experimental and is subject to change. + + +# Sift Client Library + +This library provides a high-level Python client for interacting with Sift APIs. It offers both synchronous and +asynchronous interfaces, strong type checking, and a Pythonic API design. + +## Installation + +```bash +pip install sift-stack-py +``` + +## Getting Started + +### Initializing the Client + +You can initialize the Sift client with your API key and service URLs: + +```python +from sift_client import SiftClient +from datetime import datetime + +# Initialize with individual parameters +client = SiftClient( + api_key="your-api-key", + grpc_url="your-sift-grpc-url", + rest_url="your-sift-rest-url" +) + +# Or use a connection configuration +from sift_client.transport import SiftConnectionConfig + +config = SiftConnectionConfig( + api_key="your-api-key", + grpc_url="your-sift-grpc-url", + rest_url="your-sift-rest-url" +) +client = SiftClient(connection_config=config) +``` + +The `SiftConnectionConfig` provides access to additional configuration options such as `use_ssl` and `cert_via_openssl`. + +### Using Synchronous and Asynchronous APIs + +The Sift client provides both synchronous and asynchronous versions of all APIs. You can choose the one that best fits +your application's needs. + +#### Synchronous API + +The synchronous API is perfect for scripts, notebooks, and applications that don't need asynchronous operation: + +```python +# Get an asset by ID +asset = client.assets.get(asset_id="asset123") + +# List assets with filtering +assets = client.assets.list_( + name_contains="example", + created_after=datetime(2023, 1, 1), + include_archived=False +) + +# Find a single asset matching criteria +asset = client.assets.find(name="my-asset") +``` + +#### Asynchronous API + +The asynchronous API is ideal for high-performance applications and services that need to make concurrent API calls: + +```python +import asyncio + + +async def get_asset_async(): + # Get an asset by ID asynchronously + asset = await client.assets_async.get(asset_id="asset123") + + # Running Sync within async also works + some_other_asset = client.assets.get(asset_id="asset456") + + return asset + + +# Run in an async context +asset = asyncio.run(get_asset_async()) + +``` + +### Working with Sift Types + +Sift types (like `Asset`, `Run`, etc.) are immutable Pydantic models that provide a convenient interface for working +with Sift resources. + +#### Accessing Properties + +```python +# Get an asset +asset = client.assets.get(asset_id="asset123") + +# Access properties +print(f"Asset name: {asset.name}") +print(f"Created on: {asset.created_date}") +print(f"Tags: {', '.join(asset.tags)}") +print(f"Is archived: {asset.is_archived}") +``` + +#### Using Methods on Sift Types + +Sift types have convenient methods for common operations. These methods use the synchronous API internally. +**Using these methods will update the instance in-place.** + +```python +# Get an asset +asset = client.assets.get(asset_id="asset123") + +# Archive the asset +asset.archive(archive_runs=True) + +# Update the asset +asset.update({ + "tags": ["updated", "example"] +}) +``` + +> **Note:** Type methods only work with the synchronous API. If you need to use the asynchronous API, you should use the +> resource APIs directly. + +#### Creating Update Models + +For more complex updates, you can create update models (instead of a key-value dictionary): + +```python +from sift_client.types.asset import AssetUpdate + +# Create an update model +update = AssetUpdate(tags=["new", "tags"]) + +# Apply the update +asset = client.assets.update(asset="asset123", update=update) + +# Or using the asset method +asset = client.assets.get(asset_id="asset123").update(update) +``` + +## Advanced Usage + +### Working with Tags + +Tags are a powerful way to organize and filter your assets: + +```python +# Add tags when updating an asset +asset.update({ + "tags": ["production", "model-v1", "trained"] +}) + +# Filter assets by tags +production_assets = client.assets.list_( + tags=["production"] +) +``` + +### Filtering Assets + +The client provides various ways to filter different Sift types: + +```python +# Filter by name (exact match) +assets = client.assets.list_(name="my-model") + +# Filter by name (contains) +assets = client.assets.list_(name_contains="model") + +# Filter by name (regex) +assets = client.assets.list_(name_regex="model-v[0-9]+") + +# Filter by creation date +assets = client.assets.list_( + created_after=datetime(2023, 1, 1), + created_before=datetime(2023, 12, 31) +) + +# Filter by modification date +assets = client.assets.list_( + modified_after=datetime(2023, 6, 1) +) + +# Include archived assets +all_assets = client.assets.list_(include_archived=True) + +# Limit the number of results +recent_assets = client.assets.list_( + limit=10, + order_by="modified_date desc" +) +``` + + +""" + from sift_client.client import SiftClient from sift_client.transport import SiftConnectionConfig diff --git a/python/lib/sift_client/_internal/sync_wrapper.py b/python/lib/sift_client/_internal/sync_wrapper.py index 569cab0f9..323f60c3e 100644 --- a/python/lib/sift_client/_internal/sync_wrapper.py +++ b/python/lib/sift_client/_internal/sync_wrapper.py @@ -6,6 +6,7 @@ import asyncio import inspect +import sys from functools import wraps from typing import Any, Type, TypeVar @@ -61,6 +62,7 @@ def _run(self, coro): "__doc__": f"Sync counterpart to `{name}`.\n\n{cls.__doc__ or ''}", "__init__": __init__, "_run": _run, + "__qualname__": sync_name, # Add __qualname__ to help static analyzers } # helper to wrap an async method and make into a sync method @@ -134,7 +136,15 @@ def sync_prop(self, _prop_name=prop_name): namespace[name] = _wrap_sync(name) + # Create the sync class sync_class = type(sync_name, (object,), namespace) # noqa + + # Register the class in the module's globals + # This helps static analysis tools recognize it as a proper class + if module in sys.modules: + module_globals = sys.modules[module].__dict__ + module_globals[sync_name] = sync_class + _registered.append(SyncAPIRegistration(async_cls=cls, sync_cls=sync_class)) return sync_class diff --git a/python/lib/sift_client/tests/__init__.py b/python/lib/sift_client/_tests/__init__.py similarity index 100% rename from python/lib/sift_client/tests/__init__.py rename to python/lib/sift_client/_tests/__init__.py diff --git a/python/lib/sift_client/tests/_internal/__init__.py b/python/lib/sift_client/_tests/_internal/__init__.py similarity index 100% rename from python/lib/sift_client/tests/_internal/__init__.py rename to python/lib/sift_client/_tests/_internal/__init__.py diff --git a/python/lib/sift_client/tests/_internal/test_gen_pyi.py b/python/lib/sift_client/_tests/_internal/test_gen_pyi.py similarity index 100% rename from python/lib/sift_client/tests/_internal/test_gen_pyi.py rename to python/lib/sift_client/_tests/_internal/test_gen_pyi.py diff --git a/python/lib/sift_client/tests/_internal/test_stub_module/__init__.py b/python/lib/sift_client/_tests/_internal/test_stub_module/__init__.py similarity index 67% rename from python/lib/sift_client/tests/_internal/test_stub_module/__init__.py rename to python/lib/sift_client/_tests/_internal/test_stub_module/__init__.py index 9fd1ebcaf..3352b01d6 100644 --- a/python/lib/sift_client/tests/_internal/test_stub_module/__init__.py +++ b/python/lib/sift_client/_tests/_internal/test_stub_module/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations from sift_client._internal.sync_wrapper import generate_sync_api -from sift_client.tests._internal.test_stub_module.test_py import MockClassAsync +from sift_client._tests._internal.test_stub_module.test_py import MockClassAsync MockClass: type = generate_sync_api(MockClassAsync, "MockClass") diff --git a/python/lib/sift_client/tests/_internal/test_stub_module/test_py.py b/python/lib/sift_client/_tests/_internal/test_stub_module/test_py.py similarity index 100% rename from python/lib/sift_client/tests/_internal/test_stub_module/test_py.py rename to python/lib/sift_client/_tests/_internal/test_stub_module/test_py.py diff --git a/python/lib/sift_client/tests/_internal/test_sync_wrapper.py b/python/lib/sift_client/_tests/_internal/test_sync_wrapper.py similarity index 100% rename from python/lib/sift_client/tests/_internal/test_sync_wrapper.py rename to python/lib/sift_client/_tests/_internal/test_sync_wrapper.py diff --git a/python/lib/sift_client/tests/integrated/calculated_channels.py b/python/lib/sift_client/_tests/integrated/calculated_channels.py similarity index 100% rename from python/lib/sift_client/tests/integrated/calculated_channels.py rename to python/lib/sift_client/_tests/integrated/calculated_channels.py diff --git a/python/lib/sift_client/tests/integrated/channels.py b/python/lib/sift_client/_tests/integrated/channels.py similarity index 100% rename from python/lib/sift_client/tests/integrated/channels.py rename to python/lib/sift_client/_tests/integrated/channels.py diff --git a/python/lib/sift_client/tests/integrated/runs.py b/python/lib/sift_client/_tests/integrated/runs.py similarity index 100% rename from python/lib/sift_client/tests/integrated/runs.py rename to python/lib/sift_client/_tests/integrated/runs.py diff --git a/python/lib/sift_client/tests/util/__init__.py b/python/lib/sift_client/_tests/util/__init__.py similarity index 100% rename from python/lib/sift_client/tests/util/__init__.py rename to python/lib/sift_client/_tests/util/__init__.py diff --git a/python/lib/sift_client/tests/util/test_cel_utils.py b/python/lib/sift_client/_tests/util/test_cel_utils.py similarity index 100% rename from python/lib/sift_client/tests/util/test_cel_utils.py rename to python/lib/sift_client/_tests/util/test_cel_utils.py diff --git a/python/lib/sift_client/client.py b/python/lib/sift_client/client.py index 476c4577b..c5623a5c9 100644 --- a/python/lib/sift_client/client.py +++ b/python/lib/sift_client/client.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import NamedTuple - from sift_client.errors import _sift_client_experimental_warning from sift_client.resources import ( AssetsAPI, @@ -22,23 +20,11 @@ WithGrpcClient, WithRestClient, ) +from sift_client.util.util import AsyncAPIs _sift_client_experimental_warning() -class AsyncAPIs(NamedTuple): - """Simple accessor for the asynchronous APIs, still uses the SiftClient instance.""" - - """Instance of the Ping API for making asynchronous requests.""" - ping: PingAPIAsync - """Instance of the Assets API for making asynchronous requests.""" - assets: AssetsAPIAsync - """Instance of the Calculated Channels API for making asynchronous requests.""" - calculated_channels: CalculatedChannelsAPIAsync - """Instance of the Runs API for making asynchronous requests.""" - runs: RunsAPIAsync - - class SiftClient( WithGrpcClient, WithRestClient, @@ -48,7 +34,10 @@ class SiftClient( It provides both synchronous and asynchronous interfaces, strong type checking, and a Pythonic API design. - Example usage: + !!! warning + The Sift Client is experimental and is subject to change. + + Examples: from sift_client import SiftClient from datetime import datetime @@ -72,14 +61,20 @@ class SiftClient( response = await sift.async_.ping.ping() """ - """Instance of the Ping API for making synchronous requests.""" ping: PingAPI - """Instance of the Assets API for making synchronous requests.""" + """Instance of the Ping API for making synchronous requests.""" + assets: AssetsAPI - """Instance of the Calculated Channels API for making synchronous requests.""" + """Instance of the Assets API for making synchronous requests.""" + calculated_channels: CalculatedChannelsAPI - """Instance of the Runs API for making synchronous requests.""" + """Instance of the Calculated Channels API for making synchronous requests.""" + runs: RunsAPI + """Instance of the Runs API for making synchronous requests.""" + + async_: AsyncAPIs + """Accessor for the asynchronous APIs. All asynchronous APIs are available as attributes on this accessor.""" def __init__( self, diff --git a/python/lib/sift_client/resources/py.typed b/python/lib/sift_client/resources/py.typed new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/python/lib/sift_client/resources/py.typed @@ -0,0 +1 @@ + diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.py b/python/lib/sift_client/resources/sync_stubs/__init__.py index 53628cb13..a33bb267c 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.py +++ b/python/lib/sift_client/resources/sync_stubs/__init__.py @@ -15,3 +15,5 @@ AssetsAPI = generate_sync_api(AssetsAPIAsync, "AssetsAPI") CalculatedChannelsAPI = generate_sync_api(CalculatedChannelsAPIAsync, "CalculatedChannelsAPI") RunsAPI = generate_sync_api(RunsAPIAsync, "RunsAPI") + +__all__ = ["PingAPI", "AssetsAPI", "CalculatedChannelsAPI", "RunsAPI"] diff --git a/python/lib/sift_client/util/util.py b/python/lib/sift_client/util/util.py new file mode 100644 index 000000000..d9f74ab5a --- /dev/null +++ b/python/lib/sift_client/util/util.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from typing import NamedTuple + +from sift_client.resources import ( + AssetsAPIAsync, + CalculatedChannelsAPIAsync, + PingAPIAsync, + RunsAPIAsync, +) + + +class AsyncAPIs(NamedTuple): + """Simple accessor for the asynchronous APIs, still uses the SiftClient instance.""" + + ping: PingAPIAsync + """Instance of the Ping API for making asynchronous requests.""" + + assets: AssetsAPIAsync + """Instance of the Assets API for making asynchronous requests.""" + + calculated_channels: CalculatedChannelsAPIAsync + """Instance of the Calculated Channels API for making asynchronous requests.""" + + runs: RunsAPIAsync + """Instance of the Runs API for making asynchronous requests.""" diff --git a/python/mkdocs.yml b/python/mkdocs.yml new file mode 100644 index 000000000..e598f2d8d --- /dev/null +++ b/python/mkdocs.yml @@ -0,0 +1,123 @@ +site_name: Sift | Python Client Library +site_url: https://sift-stack.github.io/sift/python/ +repo_url: https://github.com/sift-stack/sift/tree/main/python +copyright: "Copyright 2025 Sift Stack, Inc." +theme: + name: material + custom_dir: docs/overrides + favicon: stylesheets/sift-favicon.ico + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: custom + accent: custom + toggle: + icon: material/brightness-7 + name: Switch to dark mode + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: custom + accent: custom + toggle: + icon: material/brightness-4 + name: Switch to system preference + logo_light_mode: stylesheets/sift_logo_dark.svg + logo_dark_mode: stylesheets/sift_logo_light.svg + font: + text: IBM Plex Sans + code: IBM Plex Mono + features: + - navigation.instant + - navigation.tabs + - navigation.sections + - navigation.tracking + - navigation.prune + - toc.follow + # - toc.integrate + - navigation.top + - search.suggest + - content.code.copy + +extra: + version: + provider: mike + alias: true + +nav: + - Home: index.md + +plugins: + - search + - autorefs + - mike: # For docs versioning + deploy_prefix: 'python' # In case we want to use doc sites for other client libs too + - mkdocstrings: + default_handler: python + handlers: + python: + import: + - https://docs.python.org/3/objects.inv + options: + show_docstring_examples: false + load_external_modules: true + show_source: false + find_stubs_package: true + show_if_no_docstring: true + filters: "public" + show_submodules: false + # Styling + group_by_category: true + docstring_section_style: spacy + docstring_style: "google" + heading_level: 1 + merge_init_into_class: true + separate_signature: true + show_root_heading: true + show_signature_annotations: true + signature_crossrefs: true + show_symbol_type_heading: true + show_symbol_type_toc: true + summary: true + # Custom Griffe extension to inspect the sync stubs and generate their signatures + extensions: + - griffe_extensions/sync_stubs_inspector.py:InspectSpecificObjects: + paths: + - sift_client.resources.sync_stubs + + - api-autonav: + nav_section_title: Sift Py API + modules: [ lib/sift_py ] + exclude_private: true + nav_item_prefix: "" + + - api-autonav: + nav_section_title: Sift Client API (New) + modules: [ lib/sift_client ] + exclude_private: true + nav_item_prefix: "" + + +markdown_extensions: + - admonition + - toc: + permalink: true + title: On this page + - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + + +extra_css: + - stylesheets/extra.css \ No newline at end of file diff --git a/python/pyproject.toml b/python/pyproject.toml index c55580b85..fe5735604 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -52,14 +52,16 @@ development = [ "pytest-asyncio==0.23.7", "pytest-benchmark==4.0.0", "pytest-mock==3.14.0", - "ruff", + "ruff~=0.12.10", ] build = ["pdoc==14.5.0", "build==1.2.1"] +docs = ["mkdocs", "mkdocs-material", "mkdocstrings[python]", "mkdocs-include-markdown-plugin", "mkdocs-api-autonav", "mike"] openssl = ["pyOpenSSL<24.0.0", "types-pyOpenSSL<24.0.0", "cffi~=1.14"] tdms = ["npTDMS~=1.9"] rosbags = ["rosbags~=0.0"] hdf5 = ["h5py~=3.11", "polars~=1.8"] + [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta"