From 370c272e1858f3309db3f0d58e8b4896860ef1b0 Mon Sep 17 00:00:00 2001 From: Bryan Ray Date: Fri, 28 Aug 2026 10:30:14 -0500 Subject: [PATCH 1/4] chore: update repository metadata URLs --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7d82628..5133fcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ From 25c4bcfbc77ef59a8fb9655df65ad4c79aaace14 Mon Sep 17 00:00:00 2001 From: Bryan Ray Date: Fri, 28 Aug 2026 10:49:49 -0500 Subject: [PATCH 2/4] feat: add discoverable asset fieldsets --- README.md | 31 ++++ src/corva_api_client/resources/__init__.py | 19 ++- src/corva_api_client/resources/assets.py | 159 ++++++++++++++++++++- tests/test_assets_resource.py | 61 +++++++- 4 files changed, 262 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2782e1b..4f1d7c7 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,37 @@ 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. + ## Configuration `CorvaConfig.from_env()` reads these environment variables: diff --git a/src/corva_api_client/resources/__init__.py b/src/corva_api_client/resources/__init__.py index 31749f6..0a1081e 100644 --- a/src/corva_api_client/resources/__init__.py +++ b/src/corva_api_client/resources/__init__.py @@ -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 @@ -55,11 +65,16 @@ "AppStoreArticlesClient", "AppStreamClient", "AppsClient", + "AssetField", + "AssetFieldValue", + "AssetRelationship", "AssetStatus", "AssetsClient", "AuditsClient", "ColumnMapperTemplatesClient", "CompaniesClient", + "CompanyField", + "DEFAULT_ASSET_FIELDS", "DashboardAppAnnotationsClient", "DashboardsClient", "DataClient", @@ -80,6 +95,8 @@ "SecurityClient", "TasksClient", "UsersClient", + "ViewerLineField", + "ViewerPadField", "WellViewClient", "WellsClient", "WorkflowsClient", diff --git a/src/corva_api_client/resources/assets.py b/src/corva_api_client/resources/assets.py index 002a6d8..26a7503 100644 --- a/src/corva_api_client/resources/assets.py +++ b/src/corva_api_client/resources/assets.py @@ -19,6 +19,144 @@ class AssetStatus(StrEnum): ARCHIVED = "archived" +class AssetField(StrEnum): + """Sparse fieldset values supported by the v2 asset serializer.""" + + NAME = "asset.name" + ASSET_TYPE = "asset.asset_type" + STATUS = "asset.status" + TYPE = "asset.type" + STATS = "asset.stats" + LAST_ACTIVE_AT = "asset.last_active_at" + LAST_DRILLING_AT = "asset.last_drilling_at" + LAST_COMPLETION_AT = "asset.last_completion_at" + CREATED_AT = "asset.created_at" + SETTINGS = "asset.settings" + VISIBILITY = "asset.visibility" + COUNTY = "asset.county" + BASIN = "asset.basin" + API_NUMBER = "asset.api_number" + TIMEZONE = "asset.timezone" + TOP_HOLE = "asset.top_hole" + BOTTOM_HOLE = "asset.bottom_hole" + CONTRACTOR_NAME = "asset.contractor_name" + DIRECTIONAL_DRILLER = "asset.directional_driller" + MUD_COMPANY = "asset.mud_company" + COMPANY_ID = "asset.company_id" + COMPANY_NAME = "asset.company_name" + ROOT_ASSET_ID = "asset.root_asset_id" + ROOT_ASSET_NAME = "asset.root_asset_name" + PARENT_ASSET_ID = "asset.parent_asset_id" + PARENT_ASSET_NAME = "asset.parent_asset_name" + CUSTOM_PROPERTIES = "asset.custom_properties" + STRING_DESIGN = "asset.string_design" + TARGET_FORMATION = "asset.target_formation" + AREA = "asset.area" + ENABLE_ALERTS = "asset.enable_alerts" + VISIBLE_RERUN_ID = "asset.visible_rerun_id" + DAY_SHIFT_START_TIME = "asset.day_shift_start_time" + LON_LAT = "asset.lon_lat" + QC_BY = "asset.qc_by" + QC_AT = "asset.qc_at" + STANDARD_TARGET_FORMATION = "asset.standard_target_formation" + RIG_CLASSIFICATION = "asset.rig_classification" + CUSTOMER_WELL_ID = "asset.customer_well_id" + AIR_GAP = "asset.air_gap" + GROUND_ELEVATION = "asset.ground_elevation" + WATER_DEPTH = "asset.water_depth" + ORIGINAL_WELL_ID = "asset.original_well_id" + ORIGINAL_WELL_CREATED_AT = "asset.original_well_created_at" + ALTERNATIVE_NAMES = "asset.alternative_names" + ALTERNATIVE_WELLBORE_NAMES = "asset.alternative_wellbore_names" + STATE = "asset.state" + EDR_PROVIDER = "asset.edr_provider" + WELLBORE_NAME = "asset.wellbore_name" + WELL_ID = "asset.well_id" + FLOATING_VESSEL_SUBTYPE = "asset.floating_vessel_subtype" + PLATFORM_RIG_TYPE = "asset.platform_rig_type" + LAST_INTERVENTION_AT = "asset.last_intervention_at" + COUNTRY = "asset.country" + FORCE_ALERT_NOTIFICATIONS = "asset.force_alert_notifications" + SOURCE_WELL = "asset.source_well" + CREATED_BY = "asset.created_by" + RERUN_ORIGINAL_MANDATORY_FIELDS = "asset.rerun_original_mandatory_fields" + RIG_CHANGED_AT = "asset.rig_changed_at" + MERGING_RERUN_AS_SOURCE = "asset.merging_rerun_as_source" + MERGING_RERUN_AS_TARGET = "asset.merging_rerun_as_target" + RUNNING_RERUN_AS_SOURCE = "asset.running_rerun_as_source" + RUNNING_RERUN_AS_TARGET = "asset.running_rerun_as_target" + RIG_ID = "asset.rig_id" + VIEWER_PAD_ID = "asset.viewer_pad_id" + VIEWER_PAD_NAME = "asset.viewer_pad_name" + + +class AssetRelationship(StrEnum): + """Asset relationship fields that opt related records into the response.""" + + COMPANY = "asset.company" + PARENT_ASSET = "asset.parent_asset" + CHILDREN = "asset.children" + ACTIVE_CHILD = "asset.active_child" + LAST_ACTIVE_CHILD = "asset.last_active_child" + VIEWER_PAD = "asset.viewer_pad" + VIEWER_LINES = "asset.viewer_lines" + + +class CompanyField(StrEnum): + """Company fields usable when ``asset.company`` is requested.""" + + NAME = "company.name" + TIME_ZONE = "company.time_zone" + LANGUAGE = "company.language" + PROVIDER = "company.provider" + UNIT_SYSTEM = "company.unit_system" + DEV_CENTER_ENABLED = "company.dev_center_enabled" + COMPETITOR_ANALYSIS_ENABLED = "company.competitor_analysis_enabled" + ALERT_WORKFLOW_ID = "company.alert_workflow_id" + ALERT_RBAC_ENABLED = "company.alert_rbac_enabled" + TOOL_ORDERING_ENABLED = "company.tool_ordering_enabled" + WITH_SUBSCRIPTION = "company.with_subscription" + + +class ViewerPadField(StrEnum): + """Pad fields usable when ``asset.viewer_pad`` is requested.""" + + NAME = "pad.name" + LON_LAT = "pad.lon_lat" + CURRENT_FRAC_FLEET_ID = "pad.current_frac_fleet_id" + LAST_ACTIVE_AT = "pad.last_active_at" + CREATED_AT = "pad.created_at" + UPDATED_AT = "pad.updated_at" + + +class ViewerLineField(StrEnum): + """Viewer-line fields usable when ``asset.viewer_lines`` is requested.""" + + NAME = "frac_fleet_line.name" + LINE_TYPE = "frac_fleet_line.line_type" + PAD_FRAC_FLEET_ID = "frac_fleet_line.pad_frac_fleet_id" + + +AssetFieldValue = ( + AssetField | AssetRelationship | CompanyField | ViewerPadField | ViewerLineField | str +) + +DEFAULT_ASSET_FIELDS: tuple[AssetField, ...] = ( + AssetField.NAME, + AssetField.ASSET_TYPE, + AssetField.STATUS, +) + + +def _serialize_asset_fields( + fields: str | Sequence[AssetFieldValue] | None, +) -> str | None: + if fields is None or isinstance(fields, str): + return fields + + return ",".join(value for field in fields if (value := str(field).strip())) or None + + def _serialize_asset_types(types: str | Sequence[str] | None) -> list[str]: if types is None: return [] @@ -75,7 +213,7 @@ def search( types: str | Sequence[str] | None = None, status: AssetStatus | Sequence[AssetStatus] | None = None, company_id: int | None = None, - fields: str | None = "*", + fields: str | Sequence[AssetFieldValue] | None = DEFAULT_ASSET_FIELDS, start: int | None = None, end: int | None = None, sort: str | None = "-last_active_at", @@ -84,6 +222,20 @@ def search( order: str | None = None, visibility: str | None = None, ): + """Search assets using a discoverable JSON:API sparse fieldset. + + ``fields`` accepts enum members, arbitrary ``.`` + strings, comma-separated strings, or the explicit full-payload values + ``"*"`` and ``"all"``. Relationships are included only when their + :class:`AssetRelationship` value is selected. For example, request both + ``AssetRelationship.COMPANY`` and ``CompanyField.NAME`` to include the + related company's name. Pass ``None`` to omit the parameter and use the + server default. + + The default fieldset contains only the asset name, type, and status to + keep responses suitable for interactive tools and agents. Requesting + ``"*"`` or ``"all"`` can produce a substantially larger payload. + """ params: dict[str, Any] = {} if query: @@ -96,8 +248,9 @@ def search( params["status[]"] = serialized_status if company_id is not None: params["company_id"] = company_id - if fields: - params["fields"] = fields + serialized_fields = _serialize_asset_fields(fields) + if serialized_fields: + params["fields"] = serialized_fields if start is not None: params["start"] = start if end is not None: diff --git a/tests/test_assets_resource.py b/tests/test_assets_resource.py index 11e8f8c..cbae389 100644 --- a/tests/test_assets_resource.py +++ b/tests/test_assets_resource.py @@ -6,7 +6,13 @@ import httpx import pytest -from corva_api_client.resources import AssetsClient, AssetStatus +from corva_api_client.resources import ( + AssetField, + AssetRelationship, + AssetsClient, + AssetStatus, + CompanyField, +) def test_search_includes_visibility() -> None: @@ -20,7 +26,7 @@ def test_search_includes_visibility() -> None: client.get.assert_called_once_with( "/v2/assets", params={ - "fields": "*", + "fields": "asset.name,asset.asset_type,asset.status", "sort": "-last_active_at", "visibility": "company", }, @@ -55,7 +61,7 @@ def test_search_encodes_multiple_types_as_repeated_array_parameters() -> None: assert params.multi_items() == [ ("types[]", "well"), ("types[]", "rig"), - ("fields", "*"), + ("fields", "asset.name,asset.asset_type,asset.status"), ("sort", "-last_active_at"), ] @@ -97,7 +103,7 @@ def test_search_encodes_multiple_statuses_as_repeated_array_parameters() -> None assert params.multi_items() == [ ("status[]", "active"), ("status[]", "paused"), - ("fields", "*"), + ("fields", "asset.name,asset.asset_type,asset.status"), ("sort", "-last_active_at"), ] @@ -119,3 +125,50 @@ def test_search_omits_empty_status_collection() -> None: assets.search(status=[]) assert "status[]" not in client.get.call_args.kwargs["params"] + + +def test_search_serializes_discoverable_fields() -> None: + client = Mock() + assets = AssetsClient(client) + + assets.search( + fields=[ + AssetField.NAME, + AssetField.ASSET_TYPE, + AssetRelationship.COMPANY, + CompanyField.NAME, + ] + ) + + assert client.get.call_args.kwargs["params"]["fields"] == ( + "asset.name,asset.asset_type,asset.company,company.name" + ) + + +def test_search_accepts_custom_fields_for_forward_compatibility() -> None: + client = Mock() + assets = AssetsClient(client) + + assets.search(fields=[AssetField.NAME, "asset.future_field"]) + + assert client.get.call_args.kwargs["params"]["fields"] == ("asset.name,asset.future_field") + + +@pytest.mark.parametrize("fields", ["*", "all"]) +def test_search_supports_explicit_full_fieldsets(fields: str) -> None: + client = Mock() + assets = AssetsClient(client) + + assets.search(fields=fields) + + assert client.get.call_args.kwargs["params"]["fields"] == fields + + +@pytest.mark.parametrize("fields", [None, []]) +def test_search_omits_empty_fields(fields) -> None: + client = Mock() + assets = AssetsClient(client) + + assets.search(fields=fields) + + assert "fields" not in client.get.call_args.kwargs["params"] From 943d033ad25c5f0a665d7d5abf0ec97f431bb245 Mon Sep 17 00:00:00 2001 From: Bryan Ray Date: Fri, 28 Aug 2026 12:22:59 -0500 Subject: [PATCH 3/4] chore: add asset field maintenance checks --- .github/workflows/ci.yml | 4 +- CHANGELOG.md | 18 +++++ README.md | 7 ++ justfile | 15 +++-- scripts/check_asset_fields.py | 120 ++++++++++++++++++++++++++++++++++ 5 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 scripts/check_asset_fields.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b6c397..efc7498 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..31943a0 --- /dev/null +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 4f1d7c7..7d8b78c 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,13 @@ compatibility. Pass `fields="*"` or `fields="all"` only when every supported att 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: diff --git a/justfile b/justfile index 11dcf02..17f66bd 100644 --- a/justfile +++ b/justfile @@ -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 @@ -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 @@ -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 diff --git a/scripts/check_asset_fields.py b/scripts/check_asset_fields.py new file mode 100644 index 0000000..6e47dfb --- /dev/null +++ b/scripts/check_asset_fields.py @@ -0,0 +1,120 @@ +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().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() + index_action = source.split("def index", maxsplit=1)[1].split("def show", maxsplit=1)[0] + 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()) From d5c1fdbb306da963e72c36b518b30da7f89d3744 Mon Sep 17 00:00:00 2001 From: Bryan Ray Date: Fri, 28 Aug 2026 12:38:27 -0500 Subject: [PATCH 4/4] fix: harden asset field handling --- scripts/check_asset_fields.py | 16 +++++++++--- src/corva_api_client/resources/assets.py | 7 ++--- tests/test_assets_resource.py | 11 ++++++++ tests/test_check_asset_fields.py | 33 ++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 tests/test_check_asset_fields.py diff --git a/scripts/check_asset_fields.py b/scripts/check_asset_fields.py index 6e47dfb..45538f6 100644 --- a/scripts/check_asset_fields.py +++ b/scripts/check_asset_fields.py @@ -15,7 +15,7 @@ def _serializer_attributes(path: Path) -> set[str]: - lines = path.read_text().splitlines() + lines = path.read_text(encoding="utf-8").splitlines() attributes: set[str] = set() index = 0 @@ -39,8 +39,18 @@ def _serializer_attributes(path: Path) -> set[str]: def _asset_relationships(path: Path) -> set[str]: - source = path.read_text() - index_action = source.split("def index", maxsplit=1)[1].split("def show", maxsplit=1)[0] + 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: diff --git a/src/corva_api_client/resources/assets.py b/src/corva_api_client/resources/assets.py index 26a7503..168c248 100644 --- a/src/corva_api_client/resources/assets.py +++ b/src/corva_api_client/resources/assets.py @@ -151,10 +151,11 @@ class ViewerLineField(StrEnum): def _serialize_asset_fields( fields: str | Sequence[AssetFieldValue] | None, ) -> str | None: - if fields is None or isinstance(fields, str): - return fields + if fields is None: + return None - return ",".join(value for field in fields if (value := str(field).strip())) or None + raw_fields = fields.split(",") if isinstance(fields, str) else fields + return ",".join(value for field in raw_fields if (value := str(field).strip())) or None def _serialize_asset_types(types: str | Sequence[str] | None) -> list[str]: diff --git a/tests/test_assets_resource.py b/tests/test_assets_resource.py index cbae389..af8f04e 100644 --- a/tests/test_assets_resource.py +++ b/tests/test_assets_resource.py @@ -154,6 +154,17 @@ def test_search_accepts_custom_fields_for_forward_compatibility() -> None: assert client.get.call_args.kwargs["params"]["fields"] == ("asset.name,asset.future_field") +def test_search_normalizes_comma_separated_field_string() -> None: + client = Mock() + assets = AssetsClient(client) + + assets.search(fields=" asset.name, asset.status, ,asset.last_active_at ") + + assert client.get.call_args.kwargs["params"]["fields"] == ( + "asset.name,asset.status,asset.last_active_at" + ) + + @pytest.mark.parametrize("fields", ["*", "all"]) def test_search_supports_explicit_full_fieldsets(fields: str) -> None: client = Mock() diff --git a/tests/test_check_asset_fields.py b/tests/test_check_asset_fields.py new file mode 100644 index 0000000..9d8b7d7 --- /dev/null +++ b/tests/test_check_asset_fields.py @@ -0,0 +1,33 @@ +from pathlib import Path +from runpy import run_path +from typing import Callable, cast + +import pytest + +_asset_relationships = cast( + Callable[[Path], set[str]], + run_path("scripts/check_asset_fields.py")["_asset_relationships"], +) + + +@pytest.mark.parametrize( + ("source", "message"), + [ + ("class AssetsController\nend\n", "Could not find the index action"), + ("def index\nend\n", "Could not find the end of the index action"), + ( + "def index\n render json: []\nend\ndef show\nend\n", + "Could not find serializer_options", + ), + ], +) +def test_asset_relationship_parser_reports_controller_layout_errors( + tmp_path: Path, + source: str, + message: str, +) -> None: + controller = tmp_path / "assets_controller.rb" + controller.write_text(source, encoding="utf-8") + + with pytest.raises(ValueError, match=message): + _asset_relationships(controller)