x4d-devkit 0.49.0__tar.gz → 0.50.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/PKG-INFO +1 -1
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/pyproject.toml +1 -1
- x4d_devkit-0.50.0/tests/test_image_groups.py +249 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_sdk_api.py +1 -1
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/_image_contract.py +13 -3
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/image_clip.py +9 -0
- x4d_devkit-0.50.0/x4d_devkit/image_groups.py +83 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/image_schema.py +7 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/image_work_session.py +4 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit.egg-info/PKG-INFO +1 -1
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit.egg-info/SOURCES.txt +2 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/LICENSE +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/README.md +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/setup.cfg +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_accounts.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_annotation_contract.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_annotation_reference.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_assets_client.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_boundary.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_box.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_build_info.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_category_audit.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_clean_basemap.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_clean_basemap_client.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_cli_auth.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_client_json_contract.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_client_projects.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_clip_archives.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_contract_validation.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_coordinate_frames.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_current_state.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_current_state_identity_consumers_example.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_dataset_contract.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_dataset_identity.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_evaluate.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_export_bundle.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_image_clip.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_image_export.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_image_geometry.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_image_preannotation.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_image_reference.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_image_review.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_image_schema_upgrade.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_image_work_session.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_inference_input.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_inference_schemas.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_inference_service.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_label_schemas.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_lidar_materialization.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_loader.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_matching.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_models.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_platform_pose.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_preannotation_client.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_project_discovery.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_reconstruction_client.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_release_gate.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_roadmark_preview.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_semantic_scene_client.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_semantic_scene_contract.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_semantic_scene_independent.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_semantic_scene_native_clip.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_source_attributes.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_time_contract.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_token.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_tracking_eval.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_training_identity.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_transform.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_transform_chain.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_validation.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_vector_map.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_vector_map_geometry.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_vector_map_session.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_work_session.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_work_session_validation_reuse.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/accounts.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/annotation_contract.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/annotation_reference.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/audit.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/build_info.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/clean_basemap.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/cli.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/__init__.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/_origin.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/accounts.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/annotation_reference.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/assets.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/bags.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/capabilities.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/clean_basemap.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/client.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/clip_work_sessions.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/dataset_identity.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/image_clips.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/image_exports.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/image_preannotations.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/image_review.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/image_schemas.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/image_work_sessions.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/label_schemas.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/preannotation.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/projects.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/reconstruction.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/semantic_scene.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/services.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/consumer_identity.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/coordinate_contract.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/core/loader.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/core/models.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/core/transform.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/current_state.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/dataset_contract.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/dataset_identity.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/eval/api.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/eval/box.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/eval/detection.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/eval/tracking.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/export_bundle.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/image_export.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/image_geometry.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/image_preannotation.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/image_reference.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/image_review.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/image_schema_upgrade.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/inference/__init__.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/inference/input.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/inference/models.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/inference/schemas.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/inference/service.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/inference/tasks.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/json_contract.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/label_schema.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/lidar_materialization.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/materialize.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/project_discovery.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/roadmark_preview.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/semantic_scene_contract.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/time_contract.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/training_identity.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/validation/contract.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/validation/platform_pose.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/validation/validator.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/vector_map.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/vector_map_geometry.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit/work_session.py +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/x4d_devkit.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "x4d-devkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.50.0"
|
|
8
8
|
description = "Official X-4D external SDK and CLI for datasets, platform APIs, training bridges, and automation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from tests.image_fixtures import clip_payload, line_object
|
|
6
|
+
from x4d_devkit._image_contract import canonical
|
|
7
|
+
from x4d_devkit.image_clip import build_image_identity, parse_image_clip
|
|
8
|
+
from x4d_devkit.image_groups import (
|
|
9
|
+
IMAGE_OBJECT_GROUPS_VERSION,
|
|
10
|
+
parse_image_object_groups,
|
|
11
|
+
)
|
|
12
|
+
from x4d_devkit.image_schema import parse_image_schema_body
|
|
13
|
+
from x4d_devkit.image_schema_upgrade import additive_image_schema_changes
|
|
14
|
+
from x4d_devkit.image_work_session import (
|
|
15
|
+
IMAGE_WORK_SESSION_VERSION,
|
|
16
|
+
build_image_submit,
|
|
17
|
+
image_candidate,
|
|
18
|
+
parse_image_submit,
|
|
19
|
+
parse_image_work_session,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def grouped_payload():
|
|
24
|
+
value = clip_payload()
|
|
25
|
+
body = value["label_schema"]["body"]
|
|
26
|
+
body["categories"][0]["groupable"] = True
|
|
27
|
+
value["label_schema"]["identity"] = parse_image_schema_body(body).identity.to_dict()
|
|
28
|
+
first = line_object()
|
|
29
|
+
second = deepcopy(first)
|
|
30
|
+
second["id"] = "line_b"
|
|
31
|
+
second["attributes"] = {"visible": True}
|
|
32
|
+
second["attribute_ranges"][0]["attributes"] = {"style": "b"}
|
|
33
|
+
value["image_annotation"]["objects"] = [first, second]
|
|
34
|
+
value["image_annotation"]["object_groups"] = {
|
|
35
|
+
"contract_version": IMAGE_OBJECT_GROUPS_VERSION,
|
|
36
|
+
"groups": [
|
|
37
|
+
{
|
|
38
|
+
"id": "group_1",
|
|
39
|
+
"image_id": "image_a",
|
|
40
|
+
"view_id": "objects",
|
|
41
|
+
"object_ids": ["line_a", "line_b"],
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
value["identity"] = build_image_identity(value)
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_relations_preserve_independent_objects_and_all_source_components():
|
|
50
|
+
value = grouped_payload()
|
|
51
|
+
original = deepcopy(value)
|
|
52
|
+
clip = parse_image_clip(value)
|
|
53
|
+
assert clip.to_dict() == original
|
|
54
|
+
session = parse_image_work_session(
|
|
55
|
+
{"contract_version": IMAGE_WORK_SESSION_VERSION, "clip": value}
|
|
56
|
+
)
|
|
57
|
+
request = build_image_submit(session, value["image_annotation"]).to_dict()
|
|
58
|
+
assert (
|
|
59
|
+
parse_image_submit(request).to_dict()["image_annotation"]
|
|
60
|
+
== original["image_annotation"]
|
|
61
|
+
)
|
|
62
|
+
candidate = image_candidate(session.clip, request["image_annotation"]).to_dict()
|
|
63
|
+
assert candidate == original
|
|
64
|
+
value["image_annotation"]["object_groups"]["groups"][0]["object_ids"].clear()
|
|
65
|
+
assert clip.to_dict() == original
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_relation_changes_enter_revision_without_changing_geometry_or_source():
|
|
69
|
+
value = grouped_payload()
|
|
70
|
+
before = parse_image_clip(value)
|
|
71
|
+
annotation = deepcopy(value["image_annotation"])
|
|
72
|
+
annotation["object_groups"]["groups"][0]["id"] = "changed_group_identity"
|
|
73
|
+
after = image_candidate(before, annotation).to_dict()
|
|
74
|
+
for name in ("source_revision", "schema_fingerprint"):
|
|
75
|
+
assert after["identity"][name] == value["identity"][name]
|
|
76
|
+
for name in ("annotation_revision", "clip_revision"):
|
|
77
|
+
assert after["identity"][name] != value["identity"][name]
|
|
78
|
+
assert after["image_annotation"]["objects"] == value["image_annotation"]["objects"]
|
|
79
|
+
assert (
|
|
80
|
+
after["image_annotation"]["completion"]
|
|
81
|
+
== value["image_annotation"]["completion"]
|
|
82
|
+
)
|
|
83
|
+
annotation["object_groups"]["groups"] = []
|
|
84
|
+
ungrouped = image_candidate(before, annotation).to_dict()
|
|
85
|
+
assert (
|
|
86
|
+
ungrouped["image_annotation"]["objects"] == value["image_annotation"]["objects"]
|
|
87
|
+
)
|
|
88
|
+
assert ungrouped["image_annotation"]["object_groups"]["groups"] == []
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_existing_documents_remain_byte_identical_and_do_not_infer_groups():
|
|
92
|
+
old = clip_payload()
|
|
93
|
+
assert canonical(parse_image_clip(old).to_dict()) == canonical(old)
|
|
94
|
+
assert (
|
|
95
|
+
"groupable"
|
|
96
|
+
not in parse_image_schema_body(old["label_schema"]["body"]).to_dict()[
|
|
97
|
+
"categories"
|
|
98
|
+
][0]
|
|
99
|
+
)
|
|
100
|
+
value = grouped_payload()
|
|
101
|
+
del value["image_annotation"]["object_groups"]
|
|
102
|
+
value["identity"] = build_image_identity(value)
|
|
103
|
+
parsed = parse_image_clip(value).to_dict()
|
|
104
|
+
assert "object_groups" not in parsed["image_annotation"]
|
|
105
|
+
assert (
|
|
106
|
+
parsed["image_annotation"]["objects"][0]["geometry"]
|
|
107
|
+
== parsed["image_annotation"]["objects"][1]["geometry"]
|
|
108
|
+
)
|
|
109
|
+
assert canonical(parsed) == canonical(value)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@pytest.mark.parametrize(
|
|
113
|
+
"change",
|
|
114
|
+
[
|
|
115
|
+
"unknown_member",
|
|
116
|
+
"one_member",
|
|
117
|
+
"empty_members",
|
|
118
|
+
"duplicate_member",
|
|
119
|
+
"duplicate_group",
|
|
120
|
+
"duplicate_membership",
|
|
121
|
+
"cross_image",
|
|
122
|
+
"cross_view",
|
|
123
|
+
"disallowed_category",
|
|
124
|
+
"missing_permission",
|
|
125
|
+
"false_permission",
|
|
126
|
+
"colliding_id",
|
|
127
|
+
"unknown_version",
|
|
128
|
+
"null_fragment",
|
|
129
|
+
"extra_fragment",
|
|
130
|
+
"extra_group",
|
|
131
|
+
"object_provenance",
|
|
132
|
+
"geometry_mismatch",
|
|
133
|
+
"removed_member",
|
|
134
|
+
"non_array_members",
|
|
135
|
+
],
|
|
136
|
+
)
|
|
137
|
+
def test_invalid_relations_fail_with_paths_without_repair(change):
|
|
138
|
+
value = grouped_payload()
|
|
139
|
+
annotation = value["image_annotation"]
|
|
140
|
+
fragment = annotation["object_groups"]
|
|
141
|
+
group = fragment["groups"][0]
|
|
142
|
+
if change == "unknown_member":
|
|
143
|
+
group["object_ids"][1] = "missing"
|
|
144
|
+
elif change == "one_member":
|
|
145
|
+
group["object_ids"].pop()
|
|
146
|
+
elif change == "empty_members":
|
|
147
|
+
group["object_ids"] = []
|
|
148
|
+
elif change == "duplicate_member":
|
|
149
|
+
group["object_ids"][1] = "line_a"
|
|
150
|
+
elif change == "duplicate_group":
|
|
151
|
+
fragment["groups"].append(deepcopy(group))
|
|
152
|
+
elif change == "duplicate_membership":
|
|
153
|
+
fragment["groups"].append({**group, "id": "group_2"})
|
|
154
|
+
elif change == "cross_image":
|
|
155
|
+
group["image_id"] = "another_image"
|
|
156
|
+
elif change == "cross_view":
|
|
157
|
+
group["view_id"] = "semantics"
|
|
158
|
+
elif change == "disallowed_category":
|
|
159
|
+
annotation["objects"][1].update(
|
|
160
|
+
category_id="box",
|
|
161
|
+
attributes={},
|
|
162
|
+
attribute_ranges=[],
|
|
163
|
+
geometry={
|
|
164
|
+
"geometry_type": "box_2d",
|
|
165
|
+
"box": {"x_min": 1, "y_min": 2, "x_max": 10, "y_max": 20},
|
|
166
|
+
},
|
|
167
|
+
)
|
|
168
|
+
elif change == "missing_permission":
|
|
169
|
+
del value["label_schema"]["body"]["categories"][0]["groupable"]
|
|
170
|
+
elif change == "false_permission":
|
|
171
|
+
value["label_schema"]["body"]["categories"][0]["groupable"] = False
|
|
172
|
+
elif change == "colliding_id":
|
|
173
|
+
group["id"] = "line_a"
|
|
174
|
+
elif change == "unknown_version":
|
|
175
|
+
fragment["contract_version"] = "unknown"
|
|
176
|
+
elif change == "null_fragment":
|
|
177
|
+
annotation["object_groups"] = None
|
|
178
|
+
elif change == "extra_fragment":
|
|
179
|
+
fragment["origin"] = "split"
|
|
180
|
+
elif change == "extra_group":
|
|
181
|
+
group["origin"] = "split"
|
|
182
|
+
elif change == "object_provenance":
|
|
183
|
+
annotation["objects"][0]["split_from"] = "original"
|
|
184
|
+
elif change == "geometry_mismatch":
|
|
185
|
+
annotation["objects"][1]["geometry"]["geometry_type"] = "box_2d"
|
|
186
|
+
elif change == "removed_member":
|
|
187
|
+
annotation["objects"].pop()
|
|
188
|
+
elif change == "non_array_members":
|
|
189
|
+
group["object_ids"] = "line_a,line_b"
|
|
190
|
+
body = value["label_schema"]["body"]
|
|
191
|
+
value["label_schema"]["identity"] = parse_image_schema_body(body).identity.to_dict()
|
|
192
|
+
value["identity"] = build_image_identity(value)
|
|
193
|
+
original = deepcopy(value)
|
|
194
|
+
with pytest.raises(ValueError) as caught:
|
|
195
|
+
parse_image_clip(value)
|
|
196
|
+
assert caught.value.path
|
|
197
|
+
assert value == original
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@pytest.mark.parametrize("value", [None, 0, 1, "true", [], {}])
|
|
201
|
+
def test_grouping_permission_is_an_explicit_boolean(value):
|
|
202
|
+
body = clip_payload()["label_schema"]["body"]
|
|
203
|
+
body["categories"][0]["groupable"] = value
|
|
204
|
+
with pytest.raises(ValueError) as caught:
|
|
205
|
+
parse_image_schema_body(body)
|
|
206
|
+
assert caught.value.path.endswith("groupable")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_unknown_business_category_can_use_groups_without_a_catalogue():
|
|
210
|
+
value = grouped_payload()
|
|
211
|
+
body = value["label_schema"]["body"]
|
|
212
|
+
body["categories"][0]["id"] = "new_business_after_sdk_release"
|
|
213
|
+
body["task_views"][0]["category_ids"][0] = "new_business_after_sdk_release"
|
|
214
|
+
for row in value["image_annotation"]["objects"]:
|
|
215
|
+
row["category_id"] = "new_business_after_sdk_release"
|
|
216
|
+
value["label_schema"]["identity"] = parse_image_schema_body(body).identity.to_dict()
|
|
217
|
+
value["identity"] = build_image_identity(value)
|
|
218
|
+
assert parse_image_clip(value).to_dict() == value
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_empty_fragment_is_valid_and_is_not_injected_when_absent():
|
|
222
|
+
assert (
|
|
223
|
+
parse_image_object_groups(
|
|
224
|
+
{"contract_version": IMAGE_OBJECT_GROUPS_VERSION, "groups": []}
|
|
225
|
+
).to_dict()["groups"]
|
|
226
|
+
== []
|
|
227
|
+
)
|
|
228
|
+
value = clip_payload()
|
|
229
|
+
annotation = deepcopy(value["image_annotation"])
|
|
230
|
+
annotation["object_groups"] = {
|
|
231
|
+
"contract_version": IMAGE_OBJECT_GROUPS_VERSION,
|
|
232
|
+
"groups": [],
|
|
233
|
+
}
|
|
234
|
+
assert (
|
|
235
|
+
image_candidate(parse_image_clip(value), annotation).to_dict()[
|
|
236
|
+
"image_annotation"
|
|
237
|
+
]
|
|
238
|
+
== annotation
|
|
239
|
+
)
|
|
240
|
+
assert "object_groups" not in parse_image_clip(value).to_dict()["image_annotation"]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_grouping_permission_changes_are_not_silently_additive_upgrades():
|
|
244
|
+
old = clip_payload()["label_schema"]["body"]
|
|
245
|
+
new = deepcopy(old)
|
|
246
|
+
new["version"] = "2.0.0"
|
|
247
|
+
new["categories"][0]["groupable"] = True
|
|
248
|
+
with pytest.raises(ValueError):
|
|
249
|
+
additive_image_schema_changes(old, new)
|
|
@@ -11,7 +11,7 @@ from x4d_devkit.eval.matching import MetricData
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def test_package_version_is_current_release():
|
|
14
|
-
assert x4d_devkit.__version__ == "0.
|
|
14
|
+
assert x4d_devkit.__version__ == "0.50.0"
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
|
|
@@ -26,10 +26,20 @@ def require(condition: bool, message: str, path: str) -> None:
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def fields(
|
|
29
|
+
def fields(
|
|
30
|
+
value: Any,
|
|
31
|
+
expected: set[str],
|
|
32
|
+
path: str,
|
|
33
|
+
*,
|
|
34
|
+
optional: frozenset[str] | set[str] = frozenset(),
|
|
35
|
+
) -> None:
|
|
30
36
|
require(
|
|
31
|
-
type(value) is dict and set(value)
|
|
32
|
-
|
|
37
|
+
type(value) is dict and expected <= set(value) <= expected | optional,
|
|
38
|
+
(
|
|
39
|
+
f"expected exactly {sorted(expected)}"
|
|
40
|
+
if not optional
|
|
41
|
+
else f"expected required {sorted(expected)} and only optional {sorted(optional)}"
|
|
42
|
+
),
|
|
33
43
|
path,
|
|
34
44
|
)
|
|
35
45
|
|
|
@@ -25,6 +25,7 @@ from x4d_devkit._image_contract import (
|
|
|
25
25
|
unique_ids,
|
|
26
26
|
)
|
|
27
27
|
from x4d_devkit.image_geometry import parse_image_attribute_ranges, parse_image_geometry
|
|
28
|
+
from x4d_devkit.image_groups import validate_image_object_groups
|
|
28
29
|
from x4d_devkit.image_schema import parse_image_schema_snapshot
|
|
29
30
|
|
|
30
31
|
|
|
@@ -243,6 +244,7 @@ def validate_image_annotations(
|
|
|
243
244
|
value,
|
|
244
245
|
{"contract_version", "clip_id", "objects", "descriptions", "completion"},
|
|
245
246
|
"image_annotation",
|
|
247
|
+
optional={"object_groups"},
|
|
246
248
|
)
|
|
247
249
|
require(
|
|
248
250
|
value["contract_version"] == IMAGE_ANNOTATION_CONTRACT_VERSION
|
|
@@ -328,6 +330,13 @@ def validate_image_annotations(
|
|
|
328
330
|
path + ".attribute_ranges",
|
|
329
331
|
)
|
|
330
332
|
occupied.add((row["image_id"], row["view_id"]))
|
|
333
|
+
if "object_groups" in value:
|
|
334
|
+
validate_image_object_groups(
|
|
335
|
+
value["object_groups"],
|
|
336
|
+
objects=objects,
|
|
337
|
+
categories=categories,
|
|
338
|
+
result_ids=set(objects) | set(descriptions),
|
|
339
|
+
)
|
|
331
340
|
for name, row in descriptions.items():
|
|
332
341
|
path = "image_annotation.descriptions." + name
|
|
333
342
|
fields(row, {"id", "image_id", "view_id", "target_id", "fields"}, path)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Explicit native image object membership; no business names or edit provenance."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from x4d_devkit._image_contract import (
|
|
6
|
+
ImageDocument,
|
|
7
|
+
canonical,
|
|
8
|
+
document,
|
|
9
|
+
fields,
|
|
10
|
+
identifier,
|
|
11
|
+
require,
|
|
12
|
+
rows,
|
|
13
|
+
unique_ids,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
IMAGE_OBJECT_GROUPS_VERSION = "x4d-image-object-groups-v1"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_image_object_groups(value: Any) -> ImageDocument:
|
|
20
|
+
"""Validate the exact versioned fragment, preserving its IDs and ordering."""
|
|
21
|
+
canonical(value)
|
|
22
|
+
fields(value, {"contract_version", "groups"}, "object_groups")
|
|
23
|
+
require(
|
|
24
|
+
value["contract_version"] == IMAGE_OBJECT_GROUPS_VERSION,
|
|
25
|
+
"unsupported image object-group version",
|
|
26
|
+
"object_groups.contract_version",
|
|
27
|
+
)
|
|
28
|
+
groups = unique_ids(value["groups"], "object_groups.groups", nonempty=False)
|
|
29
|
+
occupied = set()
|
|
30
|
+
for name, group in groups.items():
|
|
31
|
+
path = "object_groups.groups." + name
|
|
32
|
+
fields(group, {"id", "image_id", "view_id", "object_ids"}, path)
|
|
33
|
+
identifier(group["image_id"], path + ".image_id")
|
|
34
|
+
identifier(group["view_id"], path + ".view_id")
|
|
35
|
+
members = rows(group["object_ids"], path + ".object_ids")
|
|
36
|
+
require(
|
|
37
|
+
len(members) >= 2,
|
|
38
|
+
"a group requires at least two objects",
|
|
39
|
+
path + ".object_ids",
|
|
40
|
+
)
|
|
41
|
+
for index, member in enumerate(members):
|
|
42
|
+
member_path = f"{path}.object_ids[{index}]"
|
|
43
|
+
identifier(member, member_path)
|
|
44
|
+
require(
|
|
45
|
+
member not in occupied,
|
|
46
|
+
"object is repeated or belongs to another group",
|
|
47
|
+
member_path,
|
|
48
|
+
)
|
|
49
|
+
occupied.add(member)
|
|
50
|
+
return document(value)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def validate_image_object_groups(
|
|
54
|
+
value: Any, *, objects: dict, categories: dict, result_ids: set[str]
|
|
55
|
+
) -> ImageDocument:
|
|
56
|
+
"""Check membership in the already validated complete Annotation context."""
|
|
57
|
+
parsed = parse_image_object_groups(value)
|
|
58
|
+
for group in parsed.to_dict()["groups"]:
|
|
59
|
+
path = "image_annotation.object_groups.groups." + group["id"]
|
|
60
|
+
require(
|
|
61
|
+
group["id"] not in result_ids,
|
|
62
|
+
"result IDs must be Clip-unique",
|
|
63
|
+
path + ".id",
|
|
64
|
+
)
|
|
65
|
+
for index, member in enumerate(group["object_ids"]):
|
|
66
|
+
member_path = f"{path}.object_ids[{index}]"
|
|
67
|
+
require(member in objects, "unknown group member", member_path)
|
|
68
|
+
row = objects[member]
|
|
69
|
+
require(
|
|
70
|
+
row["image_id"] == group["image_id"]
|
|
71
|
+
and row["view_id"] == group["view_id"],
|
|
72
|
+
"group members must share the same image and task view",
|
|
73
|
+
member_path,
|
|
74
|
+
)
|
|
75
|
+
require(
|
|
76
|
+
categories[row["category_id"]].get("groupable") is True,
|
|
77
|
+
"member category does not permit grouping",
|
|
78
|
+
member_path,
|
|
79
|
+
)
|
|
80
|
+
return parsed
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
__all__ = ["IMAGE_OBJECT_GROUPS_VERSION", "parse_image_object_groups"]
|
|
@@ -135,7 +135,14 @@ def parse_image_schema_body(value: Any) -> ImageSchemaBody:
|
|
|
135
135
|
category,
|
|
136
136
|
{"id", "display_name", "geometry_type", "color", "attributes"},
|
|
137
137
|
path,
|
|
138
|
+
optional={"groupable"},
|
|
138
139
|
)
|
|
140
|
+
if "groupable" in category:
|
|
141
|
+
require(
|
|
142
|
+
type(category["groupable"]) is bool,
|
|
143
|
+
"groupable must be boolean",
|
|
144
|
+
path + ".groupable",
|
|
145
|
+
)
|
|
139
146
|
text(category["display_name"], path + ".display_name")
|
|
140
147
|
require(
|
|
141
148
|
type(category["geometry_type"]) is str
|
|
@@ -19,6 +19,7 @@ from x4d_devkit.image_clip import (
|
|
|
19
19
|
build_image_identity,
|
|
20
20
|
parse_image_clip,
|
|
21
21
|
)
|
|
22
|
+
from x4d_devkit.image_groups import parse_image_object_groups
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
IMAGE_WORK_SESSION_VERSION = "x4d-image-work-session-v1"
|
|
@@ -89,7 +90,10 @@ def parse_image_submit(value: Any) -> ImageDocument:
|
|
|
89
90
|
value["image_annotation"],
|
|
90
91
|
{"contract_version", "clip_id", "objects", "descriptions", "completion"},
|
|
91
92
|
"image_submit.image_annotation",
|
|
93
|
+
optional={"object_groups"},
|
|
92
94
|
)
|
|
95
|
+
if "object_groups" in value["image_annotation"]:
|
|
96
|
+
parse_image_object_groups(value["image_annotation"]["object_groups"])
|
|
93
97
|
require(
|
|
94
98
|
value["image_annotation"]["clip_id"] == value["clip_id"],
|
|
95
99
|
"candidate belongs to another Clip",
|
|
@@ -27,6 +27,7 @@ tests/test_export_bundle.py
|
|
|
27
27
|
tests/test_image_clip.py
|
|
28
28
|
tests/test_image_export.py
|
|
29
29
|
tests/test_image_geometry.py
|
|
30
|
+
tests/test_image_groups.py
|
|
30
31
|
tests/test_image_preannotation.py
|
|
31
32
|
tests/test_image_reference.py
|
|
32
33
|
tests/test_image_review.py
|
|
@@ -84,6 +85,7 @@ x4d_devkit/export_bundle.py
|
|
|
84
85
|
x4d_devkit/image_clip.py
|
|
85
86
|
x4d_devkit/image_export.py
|
|
86
87
|
x4d_devkit/image_geometry.py
|
|
88
|
+
x4d_devkit/image_groups.py
|
|
87
89
|
x4d_devkit/image_preannotation.py
|
|
88
90
|
x4d_devkit/image_reference.py
|
|
89
91
|
x4d_devkit/image_review.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{x4d_devkit-0.49.0 → x4d_devkit-0.50.0}/tests/test_current_state_identity_consumers_example.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|