Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ jobs:

- name: Run quality checks
run: |
uv run ruff format --check src tests
uv run ruff check src tests
uv run ruff format --check src tests scripts
uv run ruff check src tests scripts
uv run ty check
uv run pytest

Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Changelog

## Unreleased

### Changed

- Asset searches now default to the compact sparse fieldset
`asset.name,asset.asset_type,asset.status` instead of `fields=*`. Callers that
require every serializer attribute and relationship can pass `fields="*"` or
`fields="all"` explicitly.
- Asset, relationship, company, viewer-pad, and viewer-line field enums make
supported sparse fieldsets discoverable while arbitrary strings remain
available for forward compatibility.

In a production measurement of 100 otherwise identical asset results, the
compact fieldset reduced the serialized response from 435,118 bytes to 14,056
bytes (96.8%, or approximately 31 times smaller). Actual results depend on the
selected assets and relationships.
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,44 @@ apps = client.apps.search(type="drilling")
client.close()
```

### Asset field selection

Asset searches use a compact fieldset by default to avoid returning every asset
attribute and relationship:

```python
assets = client.assets.search()
# fields=asset.name,asset.asset_type,asset.status
```

Use the exported field enums to discover and select additional data. A relationship
must be selected along with any fields needed from its related record:

```python
from corva_api_client.resources import AssetField, AssetRelationship, CompanyField

assets = client.assets.search(
fields=[
AssetField.NAME,
AssetField.COUNTRY,
AssetRelationship.COMPANY,
CompanyField.NAME,
]
)
```

The client also accepts comma-separated strings and arbitrary field names for forward
compatibility. Pass `fields="*"` or `fields="all"` only when every supported attribute
and relationship is required, because those options can produce substantially larger
responses. Pass `fields=None` to omit the parameter and use the API's default fieldset.

When the Rails asset serializers or relationship whitelist change, compare this SDK's
field enums with a local `corva-api` checkout:

```bash
just check-asset-fields /path/to/corva-api
```

## Configuration

`CorvaConfig.from_env()` reads these environment variables:
Expand Down
15 changes: 9 additions & 6 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ lock:
uv lock

format:
uv run ruff format src tests
uv run ruff format src tests scripts

format-check:
uv run ruff format --check src tests
uv run ruff format --check src tests scripts

lint:
uv run ruff check src tests
uv run ruff check src tests scripts

lint-fix:
uv run ruff check --fix src tests
uv run ruff check --fix src tests scripts

typecheck:
uv run ty check
Expand All @@ -31,8 +31,8 @@ test:
uv run pytest

check:
uv run ruff format --check src tests
uv run ruff check src tests
uv run ruff format --check src tests scripts
uv run ruff check src tests scripts
uv run ty check
uv run pytest

Expand All @@ -43,6 +43,9 @@ build:
check-dist: build
uv run twine check dist/*

check-asset-fields corva-api="../corva-api":
uv run python scripts/check_asset_fields.py {{corva-api}}

publish:
uv publish

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ classifiers = [
dependencies = ["httpx>=0.28.1", "jsonschema>=4.25.1", "pymongo>=4.16.0"]

[project.urls]
Repository = "https://github.com/bryansray/corva-api-client"
Issues = "https://github.com/bryansray/corva-api-client/issues"
Repository = "https://github.com/corva-ai/python-api-client"
Issues = "https://github.com/corva-ai/python-api-client/issues"

[dependency-groups]
dev = [
Expand Down
130 changes: 130 additions & 0 deletions scripts/check_asset_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
from __future__ import annotations

import re
import sys
from collections.abc import Iterable
from pathlib import Path

from corva_api_client.resources import (
AssetField,
AssetRelationship,
CompanyField,
ViewerLineField,
ViewerPadField,
)


def _serializer_attributes(path: Path) -> set[str]:
lines = path.read_text(encoding="utf-8").splitlines()
attributes: set[str] = set()
index = 0

while index < len(lines):
stripped = lines[index].strip()
if stripped.startswith("attributes "):
declaration = stripped
while declaration.rstrip().endswith(",") and index + 1 < len(lines):
index += 1
declaration += " " + lines[index].strip()
attributes.update(re.findall(r":([a-z][a-z0-9_]*)", declaration))
elif stripped.startswith("attribute :"):
match = re.match(r"attribute :([a-z][a-z0-9_]*)", stripped)
if match:
attributes.add(match.group(1))
index += 1

# JSON:API identifiers are emitted independently of sparse fieldsets.
attributes.discard("id")
return attributes


def _asset_relationships(path: Path) -> set[str]:
source = path.read_text(encoding="utf-8")
if "def index" not in source:
raise ValueError(f"Could not find the index action in {path}")

index_action = source.split("def index", maxsplit=1)[1]
if "def show" not in index_action:
raise ValueError(f"Could not find the end of the index action in {path}")

index_action = index_action.split("def show", maxsplit=1)[0]
if "serializer_options" not in index_action:
raise ValueError(f"Could not find serializer_options in the index action in {path}")

serializer_call = index_action.split("serializer_options", maxsplit=1)[1]
match = re.search(r"%i\[([^]]+)]", serializer_call, flags=re.DOTALL)
if not match:
raise ValueError(f"Could not find the asset relationship whitelist in {path}")
return set(match.group(1).split())


def _enum_fields(values: Iterable[str], record_type: str) -> set[str]:
prefix = f"{record_type}."
return {value.removeprefix(prefix) for value in values}


def _compare(label: str, sdk: set[str], api: set[str]) -> list[str]:
errors: list[str] = []
missing = sorted(api - sdk)
stale = sorted(sdk - api)
if missing:
errors.append(f"{label}: missing SDK values: {', '.join(missing)}")
if stale:
errors.append(f"{label}: stale SDK values: {', '.join(stale)}")
return errors


def main() -> int:
if len(sys.argv) != 2:
print("usage: check_asset_fields.py /path/to/corva-api", file=sys.stderr)
return 2

api_root = Path(sys.argv[1]).expanduser().resolve()
serializers = api_root / "app" / "serializers" / "v2"
controllers = api_root / "app" / "controllers" / "v2"

comparisons = (
(
"asset fields",
_enum_fields(AssetField, "asset"),
_serializer_attributes(serializers / "asset_serializer.rb"),
),
(
"asset relationships",
_enum_fields(AssetRelationship, "asset"),
_asset_relationships(controllers / "assets_controller.rb"),
),
(
"company fields",
_enum_fields(CompanyField, "company"),
_serializer_attributes(serializers / "company_serializer.rb"),
),
(
"viewer pad fields",
_enum_fields(ViewerPadField, "pad"),
_serializer_attributes(serializers / "pad_nested_serializer.rb"),
),
(
"viewer line fields",
_enum_fields(ViewerLineField, "frac_fleet_line"),
_serializer_attributes(serializers / "frac_fleet_line_nested_serializer.rb"),
),
)

errors = [
error
for label, sdk_fields, api_fields in comparisons
for error in _compare(label, sdk_fields, api_fields)
]
if errors:
print("Asset field definitions are out of sync:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
return 1

print("Asset field definitions match the Corva API serializers and whitelist.")
return 0


if __name__ == "__main__":
raise SystemExit(main())
19 changes: 18 additions & 1 deletion src/corva_api_client/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@
from .app_store_articles import AppStoreArticlesClient
from .app_stream import AppStreamClient
from .apps import AppsClient
from .assets import AssetsClient, AssetStatus
from .assets import (
DEFAULT_ASSET_FIELDS,
AssetField,
AssetFieldValue,
AssetRelationship,
AssetsClient,
AssetStatus,
CompanyField,
ViewerLineField,
ViewerPadField,
)
from .audits import AuditsClient
from .column_mapper_templates import ColumnMapperTemplatesClient
from .companies import CompaniesClient
Expand Down Expand Up @@ -55,11 +65,16 @@
"AppStoreArticlesClient",
"AppStreamClient",
"AppsClient",
"AssetField",
"AssetFieldValue",
"AssetRelationship",
"AssetStatus",
"AssetsClient",
"AuditsClient",
"ColumnMapperTemplatesClient",
"CompaniesClient",
"CompanyField",
"DEFAULT_ASSET_FIELDS",
"DashboardAppAnnotationsClient",
"DashboardsClient",
"DataClient",
Expand All @@ -80,6 +95,8 @@
"SecurityClient",
"TasksClient",
"UsersClient",
"ViewerLineField",
"ViewerPadField",
"WellViewClient",
"WellsClient",
"WorkflowsClient",
Expand Down
Loading