opensportslib 0.3.1.dev25__tar.gz → 0.3.1.dev27__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.
- {opensportslib-0.3.1.dev25/opensportslib.egg-info → opensportslib-0.3.1.dev27}/PKG-INFO +8 -2
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/README.md +7 -1
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27/opensportslib.egg-info}/PKG-INFO +8 -2
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib.egg-info/SOURCES.txt +8 -1
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/pyproject.toml +2 -2
- opensportslib-0.3.1.dev27/tests/helpers/causal_lm.py +110 -0
- opensportslib-0.3.1.dev27/tests/unit/api/test_server_jobs.py +121 -0
- opensportslib-0.3.1.dev27/tests/unit/api/test_server_registry.py +314 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/contracts/test_package.py +3 -2
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/core/test_classification_trainer.py +90 -0
- opensportslib-0.3.1.dev27/tests/unit/core/test_hf_runtime.py +159 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/data/test_localization_intervals.py +45 -1
- opensportslib-0.3.1.dev27/tests/unit/models/test_localization_metrics.py +129 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/models/test_vqa_qwen_xvars.py +1 -103
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/tools/test_hf_transfer.py +178 -0
- opensportslib-0.3.1.dev27/tests/unit/tools/test_spotting_classification.py +489 -0
- opensportslib-0.3.1.dev27/tools/convert/spotting_to_classification.py +902 -0
- opensportslib-0.3.1.dev27/tools/convert/spotting_tracking_to_classification.py +414 -0
- opensportslib-0.3.1.dev27/tools/upload/upload_sngar_tracking_full.py +107 -0
- opensportslib-0.3.1.dev27/tools/upload/upload_sngar_videos.py +130 -0
- opensportslib-0.3.1.dev25/tests/unit/api/test_server_jobs.py +0 -63
- opensportslib-0.3.1.dev25/tests/unit/api/test_server_registry.py +0 -131
- opensportslib-0.3.1.dev25/tests/unit/models/test_localization_metrics.py +0 -25
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/LICENSE +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/LICENSE-COMMERCIAL +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/MANIFEST.in +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/examples/quickstart/basic_classification.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/examples/quickstart/basic_localization.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/examples/quickstart/basic_vqa.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/adaptation/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/adaptation/spotta.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/apis/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/apis/base_task_model.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/apis/classification.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/apis/config.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/apis/configuration.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/apis/localization.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/apis/vqa.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/cli.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/classification/default.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/classification/sngar_frames.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/classification/sngar_tracking.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/classification/video.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/default.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/localization/calf_resnetpca512.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/localization/default.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/localization/e2e_spotta.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/localization/h5_header_distance.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/localization/h5_header_skeleton.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/localization/netvladpp_resnetpca512.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/localization/tracking_action_spotting.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/localization/video_dali.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/localization/video_ocv.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/vqa/default.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/vqa/qwen.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/vqa/qwen3_vl_native.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/vqa/qwen_lora.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/vqa/qwen_sngar_frames.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/configs/vqa/xvars.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/config/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/config/accessors.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/config/conflicts.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/config/editable.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/config/loader.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/config/migrate.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/config/migrations/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/config/migrations/legacy_to_canonical.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/config/rule_variants.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/config/runtime_adapter.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/config/schema.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/config/schemas/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/config/schemas/schema_canonical.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/config/schemas/schema_legacy.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/config/validate.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/loss/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/loss/builder.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/loss/calf.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/loss/ce.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/loss/combine.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/loss/nll.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/optimizer/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/optimizer/builder.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/sampler/weighted_sampler.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/scheduler/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/scheduler/builder.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/trainer/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/trainer/classification_trainer.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/trainer/localization_trainer.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/trainer/vqa_trainer.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/utils/checkpoint.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/utils/config.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/utils/config_normalize.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/utils/data.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/utils/ddp.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/utils/default_args.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/utils/direct_video.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/utils/hf_runtime.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/utils/lightning.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/utils/load_annotations.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/utils/seed.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/utils/video_processing.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/core/utils/wandb.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/datasets/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/datasets/builder.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/datasets/classification_dataset.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/datasets/hf_json.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/datasets/hf_tracking.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/datasets/localization_dataset.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/datasets/utils/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/datasets/utils/h5_tracking.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/datasets/utils/tracking.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/datasets/vqa_dataset.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/legacy_config/classification.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/legacy_config/localization-e2e-ocv.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/legacy_config/localization-json_calf_resnetpca512.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/legacy_config/localization-json_netvlad++_resnetpca512.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/legacy_config/localization.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/legacy_config/sngar-frames.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/legacy_config/sngar-tracking.yaml +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/metrics/classification_metric.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/metrics/localization_metric.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/metrics/vqa_metric.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/backbones/builder.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/base/contextaware.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/base/e2e.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/base/learnablepooling.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/base/qwen_vl_native.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/base/qwen_xvars.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/base/rule_based.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/base/tracking.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/base/vars.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/base/video.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/base/video_chatgpt_compat.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/base/video_mae.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/base/xvars_videochatgpt.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/builder.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/heads/builder.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/neck/builder.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/utils/common.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/utils/impl/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/utils/impl/asformer.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/utils/impl/calf.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/utils/impl/gsm.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/utils/impl/gtad.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/utils/impl/tsm.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/utils/litebase.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/utils/modules.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/utils/shift.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/utils/utils.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/utils/vqa_prediction_priors.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/utils/vqa_prompting.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/models/utils/xvars_clip_index.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/remote_registry.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/setup/setup.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/tools/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/tools/_common.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/tools/hf_transfer.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/tools/osl_json_to_parquet.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib/tools/parquet_to_osl_json.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib.egg-info/dependency_links.txt +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib.egg-info/entry_points.txt +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib.egg-info/requires.txt +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/opensportslib.egg-info/top_level.txt +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/scripts/redact_test_stream.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/scripts/release_versions.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/scripts/run_h5_header_rule_inference.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/scripts/summarize_test_report.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/scripts/verify_release_manifests.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/scripts/write_release_manifest.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/setup.cfg +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/conftest.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/helpers/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/helpers/configs.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/helpers/pipeline_stubs.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/integration/classification/test_pipeline.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/integration/localization/test_pipeline.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/integration/localization/test_spotta.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/integration/vqa/test_pipeline.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/release/__init__.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/release/_release_common.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/release/test_00_environment.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/release/test_classification_release.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/release/test_localization_release.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/release/test_vqa_release.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/smoke/test_cli.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/smoke/test_config_utils.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/smoke/test_environment.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/smoke/test_package.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/smoke/test_public_apis.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/api/test_remote_registry.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/api/test_remote_sessions.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/api/test_server_startup_scripts.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/api/test_task_model_contract.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/api/test_vqa.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/config/test_architecture.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/config/test_editable.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/config/test_optional_hf.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/config/test_pretrained_merge.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/config/test_split_overrides.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/contracts/test_architecture.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/contracts/test_dependencies.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/contracts/test_release_versions.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/contracts/test_runner_contract.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/core/test_localization_hf_backend.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/core/test_training_components.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/core/test_video_processing.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/data/test_classification_paths.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/data/test_h5_tracking.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/data/test_hf_json_spotting.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/data/test_hf_json_tracking_spotting.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/data/test_hf_tracking_dataset.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/data/test_localization_dali.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/data/test_osl_formats.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/models/test_classification_metrics.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/models/test_header_rule_spotter.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/models/test_header_skeleton_spotter.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/models/test_temporal_shift.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/models/test_vqa_metrics.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/models/test_vqa_training_lora.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/models/test_vqa_xvars_videochatgpt.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/tools/test_conversion.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/tools/test_extract_xvars_features.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/tools/test_release_manifest.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/tools/test_report_summary.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tests/unit/tools/test_setup.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/convert/build_sn_vqa_2026_vqa.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/convert/build_sngar_spotting.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/convert/build_soccernet_gar.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/convert/build_soccernet_gar_action_spotting.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/convert/build_soccernet_gar_vqa.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/convert/build_xvars_indexes.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/convert/extract_xvars_clip_features.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/convert/osl_json_to_parquet_webdataset.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/convert/parquet_webdataset_to_osl_json.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/convert/sngar_dataset_card.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/convert/sngar_events.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/convert/verify_sngar_spotting.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/download/download_hf_repo.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/download/download_osl_hf.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/download/push_sngar_spotting.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/download/upload_osl_hf.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/train/train_config.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/training/classification.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/training/localization.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/training/vqa.py +0 -0
- {opensportslib-0.3.1.dev25 → opensportslib-0.3.1.dev27}/tools/upload/upload_model_hf.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opensportslib
|
|
3
|
-
Version: 0.3.1.
|
|
3
|
+
Version: 0.3.1.dev27
|
|
4
4
|
Summary: OpenSportsLib is the professional library, designed for advanced video understanding in sports. It provides state-of-the-art tools for action recognition, spotting, retrieval, and captioning, making it ideal for researchers, analysts, and developers working with sports video data.
|
|
5
5
|
Author: Jeet Vora
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -67,7 +67,7 @@ OpenSportsLib is designed for **researchers, ML engineers, and sports analytics
|
|
|
67
67
|
|
|
68
68
|
- **Documentation:** https://opensportslab.github.io/opensportslib/
|
|
69
69
|
- **OSL JSON format:** https://opensportslab.github.io/opensportslib/data/osl-json-format/
|
|
70
|
-
- **Inference server:** [server/README.md](server/README.md)
|
|
70
|
+
- **Inference server:** [server/README.md](server/README.md), including checkpoint and config-only rule-model registration
|
|
71
71
|
- **PyPI:** https://pypi.org/project/opensportslib/
|
|
72
72
|
- **Issues:** https://github.com/OpenSportsLab/opensportslib/issues
|
|
73
73
|
|
|
@@ -324,3 +324,9 @@ video understanding. Core contributors affiliated with KAUST include:
|
|
|
324
324
|
- [Jeet Vora](https://jeetv.github.io/) — Remote Research Engineer
|
|
325
325
|
- [Dr. Merey Ramazanova](https://meryusha.github.io/) — Post-Doc
|
|
326
326
|
- [Dr. Silvio Giancola](https://www.silviogiancola.com/) — Research Scientist
|
|
327
|
+
|
|
328
|
+
For synchronized native-FPS GAR action clips and optional exact frame reproduction, see the
|
|
329
|
+
[spotting-to-classification converter](docs/tools/spotting-to-classification.md).
|
|
330
|
+
|
|
331
|
+
For full-rate tracking clips with the same classification sample IDs, see the
|
|
332
|
+
[tracking conversion script](docs/tools/spotting-tracking-to-classification.md).
|
|
@@ -28,7 +28,7 @@ OpenSportsLib is designed for **researchers, ML engineers, and sports analytics
|
|
|
28
28
|
|
|
29
29
|
- **Documentation:** https://opensportslab.github.io/opensportslib/
|
|
30
30
|
- **OSL JSON format:** https://opensportslab.github.io/opensportslib/data/osl-json-format/
|
|
31
|
-
- **Inference server:** [server/README.md](server/README.md)
|
|
31
|
+
- **Inference server:** [server/README.md](server/README.md), including checkpoint and config-only rule-model registration
|
|
32
32
|
- **PyPI:** https://pypi.org/project/opensportslib/
|
|
33
33
|
- **Issues:** https://github.com/OpenSportsLab/opensportslib/issues
|
|
34
34
|
|
|
@@ -285,3 +285,9 @@ video understanding. Core contributors affiliated with KAUST include:
|
|
|
285
285
|
- [Jeet Vora](https://jeetv.github.io/) — Remote Research Engineer
|
|
286
286
|
- [Dr. Merey Ramazanova](https://meryusha.github.io/) — Post-Doc
|
|
287
287
|
- [Dr. Silvio Giancola](https://www.silviogiancola.com/) — Research Scientist
|
|
288
|
+
|
|
289
|
+
For synchronized native-FPS GAR action clips and optional exact frame reproduction, see the
|
|
290
|
+
[spotting-to-classification converter](docs/tools/spotting-to-classification.md).
|
|
291
|
+
|
|
292
|
+
For full-rate tracking clips with the same classification sample IDs, see the
|
|
293
|
+
[tracking conversion script](docs/tools/spotting-tracking-to-classification.md).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opensportslib
|
|
3
|
-
Version: 0.3.1.
|
|
3
|
+
Version: 0.3.1.dev27
|
|
4
4
|
Summary: OpenSportsLib is the professional library, designed for advanced video understanding in sports. It provides state-of-the-art tools for action recognition, spotting, retrieval, and captioning, making it ideal for researchers, analysts, and developers working with sports video data.
|
|
5
5
|
Author: Jeet Vora
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -67,7 +67,7 @@ OpenSportsLib is designed for **researchers, ML engineers, and sports analytics
|
|
|
67
67
|
|
|
68
68
|
- **Documentation:** https://opensportslab.github.io/opensportslib/
|
|
69
69
|
- **OSL JSON format:** https://opensportslab.github.io/opensportslib/data/osl-json-format/
|
|
70
|
-
- **Inference server:** [server/README.md](server/README.md)
|
|
70
|
+
- **Inference server:** [server/README.md](server/README.md), including checkpoint and config-only rule-model registration
|
|
71
71
|
- **PyPI:** https://pypi.org/project/opensportslib/
|
|
72
72
|
- **Issues:** https://github.com/OpenSportsLab/opensportslib/issues
|
|
73
73
|
|
|
@@ -324,3 +324,9 @@ video understanding. Core contributors affiliated with KAUST include:
|
|
|
324
324
|
- [Jeet Vora](https://jeetv.github.io/) — Remote Research Engineer
|
|
325
325
|
- [Dr. Merey Ramazanova](https://meryusha.github.io/) — Post-Doc
|
|
326
326
|
- [Dr. Silvio Giancola](https://www.silviogiancola.com/) — Research Scientist
|
|
327
|
+
|
|
328
|
+
For synchronized native-FPS GAR action clips and optional exact frame reproduction, see the
|
|
329
|
+
[spotting-to-classification converter](docs/tools/spotting-to-classification.md).
|
|
330
|
+
|
|
331
|
+
For full-rate tracking clips with the same classification sample IDs, see the
|
|
332
|
+
[tracking conversion script](docs/tools/spotting-tracking-to-classification.md).
|
|
@@ -154,6 +154,7 @@ scripts/write_release_manifest.py
|
|
|
154
154
|
tests/__init__.py
|
|
155
155
|
tests/conftest.py
|
|
156
156
|
tests/helpers/__init__.py
|
|
157
|
+
tests/helpers/causal_lm.py
|
|
157
158
|
tests/helpers/configs.py
|
|
158
159
|
tests/helpers/pipeline_stubs.py
|
|
159
160
|
tests/integration/classification/test_pipeline.py
|
|
@@ -189,6 +190,7 @@ tests/unit/contracts/test_package.py
|
|
|
189
190
|
tests/unit/contracts/test_release_versions.py
|
|
190
191
|
tests/unit/contracts/test_runner_contract.py
|
|
191
192
|
tests/unit/core/test_classification_trainer.py
|
|
193
|
+
tests/unit/core/test_hf_runtime.py
|
|
192
194
|
tests/unit/core/test_localization_hf_backend.py
|
|
193
195
|
tests/unit/core/test_training_components.py
|
|
194
196
|
tests/unit/core/test_video_processing.py
|
|
@@ -215,6 +217,7 @@ tests/unit/tools/test_hf_transfer.py
|
|
|
215
217
|
tests/unit/tools/test_release_manifest.py
|
|
216
218
|
tests/unit/tools/test_report_summary.py
|
|
217
219
|
tests/unit/tools/test_setup.py
|
|
220
|
+
tests/unit/tools/test_spotting_classification.py
|
|
218
221
|
tools/convert/build_sn_vqa_2026_vqa.py
|
|
219
222
|
tools/convert/build_sngar_spotting.py
|
|
220
223
|
tools/convert/build_soccernet_gar.py
|
|
@@ -226,6 +229,8 @@ tools/convert/osl_json_to_parquet_webdataset.py
|
|
|
226
229
|
tools/convert/parquet_webdataset_to_osl_json.py
|
|
227
230
|
tools/convert/sngar_dataset_card.py
|
|
228
231
|
tools/convert/sngar_events.py
|
|
232
|
+
tools/convert/spotting_to_classification.py
|
|
233
|
+
tools/convert/spotting_tracking_to_classification.py
|
|
229
234
|
tools/convert/verify_sngar_spotting.py
|
|
230
235
|
tools/download/download_hf_repo.py
|
|
231
236
|
tools/download/download_osl_hf.py
|
|
@@ -235,4 +240,6 @@ tools/train/train_config.py
|
|
|
235
240
|
tools/training/classification.py
|
|
236
241
|
tools/training/localization.py
|
|
237
242
|
tools/training/vqa.py
|
|
238
|
-
tools/upload/upload_model_hf.py
|
|
243
|
+
tools/upload/upload_model_hf.py
|
|
244
|
+
tools/upload/upload_sngar_tracking_full.py
|
|
245
|
+
tools/upload/upload_sngar_videos.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "opensportslib"
|
|
7
|
-
version = "0.3.1.
|
|
7
|
+
version = "0.3.1.dev27"
|
|
8
8
|
description = "OpenSportsLib is the professional library, designed for advanced video understanding in sports. It provides state-of-the-art tools for action recognition, spotting, retrieval, and captioning, making it ideal for researchers, analysts, and developers working with sports video data."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -22,7 +22,7 @@ opensportslib = "opensportslib.cli:main"
|
|
|
22
22
|
opensportslib = [ "legacy_config/*.yaml", "configs/**/*.yaml",]
|
|
23
23
|
|
|
24
24
|
[tool.coverage.run]
|
|
25
|
-
source = [ "opensportslib",]
|
|
25
|
+
source = [ "opensportslib", "server",]
|
|
26
26
|
branch = true
|
|
27
27
|
omit = [ "*/.ipynb_checkpoints/*",]
|
|
28
28
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Small tokenizer and causal model doubles for offline generation tests."""
|
|
2
|
+
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TinyTokenizer:
|
|
9
|
+
pad_token_id = 0
|
|
10
|
+
eos_token_id = 1
|
|
11
|
+
pad_token = "<pad>"
|
|
12
|
+
eos_token = "</s>"
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.vocab = {"<pad>": 0, "</s>": 1, "<vid_patch>": 2, "<vid_start>": 3, "<vid_end>": 4}
|
|
16
|
+
|
|
17
|
+
def get_vocab(self):
|
|
18
|
+
return dict(self.vocab)
|
|
19
|
+
|
|
20
|
+
def add_special_tokens(self, payload):
|
|
21
|
+
added = 0
|
|
22
|
+
for token in payload.get("additional_special_tokens", []):
|
|
23
|
+
if token not in self.vocab:
|
|
24
|
+
self.vocab[token] = len(self.vocab)
|
|
25
|
+
added += 1
|
|
26
|
+
return added
|
|
27
|
+
|
|
28
|
+
def convert_tokens_to_ids(self, token):
|
|
29
|
+
return self.vocab[token]
|
|
30
|
+
|
|
31
|
+
def __len__(self):
|
|
32
|
+
return len(self.vocab)
|
|
33
|
+
|
|
34
|
+
def __call__(self, text, return_tensors=None, **kwargs):
|
|
35
|
+
del kwargs
|
|
36
|
+
if isinstance(text, list):
|
|
37
|
+
text = text[0]
|
|
38
|
+
tokens = []
|
|
39
|
+
i = 0
|
|
40
|
+
while i < len(text):
|
|
41
|
+
matched = False
|
|
42
|
+
for token in ("<vid_start>", "<vid_patch>", "<vid_end>", "</s>"):
|
|
43
|
+
if text.startswith(token, i):
|
|
44
|
+
tokens.append(token)
|
|
45
|
+
i += len(token)
|
|
46
|
+
matched = True
|
|
47
|
+
break
|
|
48
|
+
if matched:
|
|
49
|
+
continue
|
|
50
|
+
if text[i].isspace():
|
|
51
|
+
i += 1
|
|
52
|
+
continue
|
|
53
|
+
j = i
|
|
54
|
+
while j < len(text) and not text[j].isspace() and not any(
|
|
55
|
+
text.startswith(token, j) for token in ("<vid_start>", "<vid_patch>", "<vid_end>", "</s>")
|
|
56
|
+
):
|
|
57
|
+
j += 1
|
|
58
|
+
tokens.append(text[i:j])
|
|
59
|
+
i = j
|
|
60
|
+
ids = []
|
|
61
|
+
for token in tokens:
|
|
62
|
+
if token not in self.vocab:
|
|
63
|
+
self.vocab[token] = len(self.vocab)
|
|
64
|
+
ids.append(self.vocab[token])
|
|
65
|
+
attn = [1] * len(ids)
|
|
66
|
+
if return_tensors == "pt":
|
|
67
|
+
return {"input_ids": torch.tensor([ids]), "attention_mask": torch.tensor([attn])}
|
|
68
|
+
return SimpleNamespace(input_ids=ids, attention_mask=attn)
|
|
69
|
+
|
|
70
|
+
def batch_decode(self, sequences, skip_special_tokens=True):
|
|
71
|
+
del sequences, skip_special_tokens
|
|
72
|
+
return ["qwen answer </s>"]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TinyGenerateLM(torch.nn.Module):
|
|
76
|
+
def __init__(self):
|
|
77
|
+
super().__init__()
|
|
78
|
+
self.config = SimpleNamespace(hidden_size=4, vocab_size=64, use_cache=True)
|
|
79
|
+
self.generation_config = SimpleNamespace(use_cache=False)
|
|
80
|
+
self.emb = torch.nn.Embedding(128, 4)
|
|
81
|
+
self.lm = torch.nn.Linear(4, 64)
|
|
82
|
+
self.seen_inputs_embeds = None
|
|
83
|
+
self.generate_kwargs = None
|
|
84
|
+
|
|
85
|
+
def get_input_embeddings(self):
|
|
86
|
+
return self.emb
|
|
87
|
+
|
|
88
|
+
def resize_token_embeddings(self, size):
|
|
89
|
+
return self.emb
|
|
90
|
+
|
|
91
|
+
def save_pretrained(self, output_dir):
|
|
92
|
+
del output_dir
|
|
93
|
+
|
|
94
|
+
def forward(self, input_ids=None, inputs_embeds=None, attention_mask=None, labels=None, **kwargs):
|
|
95
|
+
del input_ids, attention_mask, kwargs
|
|
96
|
+
self.seen_inputs_embeds = inputs_embeds.detach().clone()
|
|
97
|
+
logits = self.lm(inputs_embeds)
|
|
98
|
+
loss = logits.sum() * 0
|
|
99
|
+
if labels is not None:
|
|
100
|
+
loss = loss + 0.123
|
|
101
|
+
return SimpleNamespace(loss=loss, logits=logits)
|
|
102
|
+
|
|
103
|
+
def generate(self, input_ids=None, inputs_embeds=None, attention_mask=None, **kwargs):
|
|
104
|
+
del attention_mask
|
|
105
|
+
self.generate_kwargs = {"input_ids": input_ids, "inputs_embeds": inputs_embeds, **kwargs}
|
|
106
|
+
extra = torch.tensor([[9, 1]], device=input_ids.device if input_ids is not None else inputs_embeds.device)
|
|
107
|
+
if input_ids is None:
|
|
108
|
+
prefix = torch.zeros((1, inputs_embeds.shape[1]), dtype=torch.long, device=extra.device)
|
|
109
|
+
return torch.cat((prefix, extra), dim=1)
|
|
110
|
+
return torch.cat((input_ids, extra), dim=1)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
SERVER_ROOT = Path(__file__).resolve().parents[3] / "server"
|
|
9
|
+
sys.path.insert(0, str(SERVER_ROOT))
|
|
10
|
+
|
|
11
|
+
from app.schemas import JobStatus, PredictRequest # noqa: E402
|
|
12
|
+
from storage.jobs import JobStore # noqa: E402
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def store(tmp_path):
|
|
16
|
+
runtime = tmp_path / "runtime"
|
|
17
|
+
settings = SimpleNamespace(
|
|
18
|
+
runtime_dir=runtime,
|
|
19
|
+
tmp_dir=runtime / "tmp",
|
|
20
|
+
jobs_dir=runtime / "jobs",
|
|
21
|
+
results_dir=runtime / "results",
|
|
22
|
+
)
|
|
23
|
+
settings.jobs_dir.mkdir(parents=True)
|
|
24
|
+
settings.tmp_dir.mkdir(parents=True)
|
|
25
|
+
settings.results_dir.mkdir(parents=True)
|
|
26
|
+
return JobStore(settings)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def request():
|
|
30
|
+
return PredictRequest(task_type="classification", video_path="/tmp/clip.mp4")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_metadata_is_valid_json_after_create(tmp_path):
|
|
34
|
+
jobs = store(tmp_path)
|
|
35
|
+
metadata = jobs.create_job(request(), "model")
|
|
36
|
+
|
|
37
|
+
loaded = jobs.load_metadata(metadata.job_id)
|
|
38
|
+
assert loaded is not None
|
|
39
|
+
assert loaded.status is JobStatus.QUEUED
|
|
40
|
+
assert jobs.metadata_path(metadata.job_id).read_text(encoding="utf-8").strip()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_metadata_replacement_never_leaves_partial_file(tmp_path):
|
|
44
|
+
jobs = store(tmp_path)
|
|
45
|
+
metadata = jobs.create_job(request(), "model")
|
|
46
|
+
|
|
47
|
+
for status in (JobStatus.RUNNING, JobStatus.SUCCEEDED, JobStatus.FAILED):
|
|
48
|
+
metadata.status = status
|
|
49
|
+
jobs.save_metadata(metadata)
|
|
50
|
+
loaded = jobs.load_metadata(metadata.job_id)
|
|
51
|
+
assert loaded is not None
|
|
52
|
+
assert loaded.status is status
|
|
53
|
+
assert not list(jobs.settings.jobs_dir.glob(f".{metadata.job_id}.*.tmp"))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_invalid_metadata_is_treated_as_missing(tmp_path):
|
|
57
|
+
jobs = store(tmp_path)
|
|
58
|
+
path = jobs.metadata_path("broken")
|
|
59
|
+
path.write_text("", encoding="utf-8")
|
|
60
|
+
|
|
61
|
+
assert jobs.load_metadata("broken") is None
|
|
62
|
+
with pytest.raises(FileNotFoundError, match="broken"):
|
|
63
|
+
jobs.mark_running("broken")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_job_lifecycle_persists_request_result_and_clears_failure(tmp_path):
|
|
67
|
+
import json
|
|
68
|
+
|
|
69
|
+
jobs = store(tmp_path)
|
|
70
|
+
metadata = jobs.create_job(request(), "model", session_id="session", input_source="uploaded.mp4")
|
|
71
|
+
job_id = metadata.job_id
|
|
72
|
+
assert jobs.load_result(job_id) is None
|
|
73
|
+
assert jobs.mark_running(job_id).status is JobStatus.RUNNING
|
|
74
|
+
assert jobs.mark_failed(job_id, "temporary failure").error == "temporary failure"
|
|
75
|
+
assert jobs.load_metadata(job_id).status is JobStatus.FAILED
|
|
76
|
+
payload = {"predictions": [{"label": "PASS", "confidence": .9}]}
|
|
77
|
+
succeeded = jobs.mark_succeeded(job_id, payload)
|
|
78
|
+
assert succeeded.status is JobStatus.SUCCEEDED and succeeded.error is None
|
|
79
|
+
assert jobs.load_result(job_id) == payload
|
|
80
|
+
assert jobs.load_metadata(job_id).session_id == "session"
|
|
81
|
+
assert jobs.load_metadata(job_id).input_source == "uploaded.mp4"
|
|
82
|
+
saved = json.loads((jobs.settings.tmp_dir / job_id / "request.json").read_text())
|
|
83
|
+
assert saved["video_path"] == "/tmp/clip.mp4"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.mark.parametrize("source", ["video_path", "media_url", "test_set_path"])
|
|
87
|
+
def test_updating_request_updates_persisted_input_source(tmp_path, source):
|
|
88
|
+
import json
|
|
89
|
+
|
|
90
|
+
jobs = store(tmp_path)
|
|
91
|
+
metadata = jobs.create_job(request(), "model")
|
|
92
|
+
replacement = PredictRequest(task_type="classification", **{source: "new-input"})
|
|
93
|
+
jobs.update_request_payload(metadata.job_id, replacement)
|
|
94
|
+
assert jobs.load_metadata(metadata.job_id).input_source == "new-input"
|
|
95
|
+
saved = json.loads((jobs.settings.tmp_dir / metadata.job_id / "request.json").read_text())
|
|
96
|
+
assert saved[source] == "new-input"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_atomic_metadata_failure_preserves_last_valid_record(tmp_path, monkeypatch):
|
|
100
|
+
import storage.jobs as module
|
|
101
|
+
|
|
102
|
+
jobs = store(tmp_path)
|
|
103
|
+
metadata = jobs.create_job(request(), "model")
|
|
104
|
+
previous = jobs.metadata_path(metadata.job_id).read_bytes()
|
|
105
|
+
def fail_replace(*args):
|
|
106
|
+
raise OSError("disk replacement failed")
|
|
107
|
+
monkeypatch.setattr(module.os, "replace", fail_replace)
|
|
108
|
+
with pytest.raises(OSError, match="replacement failed"):
|
|
109
|
+
jobs.mark_running(metadata.job_id)
|
|
110
|
+
assert jobs.metadata_path(metadata.job_id).read_bytes() == previous
|
|
111
|
+
assert not list(jobs.settings.jobs_dir.glob("*.tmp"))
|
|
112
|
+
assert jobs.load_metadata(metadata.job_id).status is JobStatus.QUEUED
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@pytest.mark.parametrize("operation", ["mark_running", "mark_failed", "mark_succeeded"])
|
|
116
|
+
def test_updates_to_missing_job_fail_without_creating_results(tmp_path, operation):
|
|
117
|
+
jobs = store(tmp_path)
|
|
118
|
+
args = {"mark_running": (), "mark_failed": ("failure",), "mark_succeeded": ({},)}[operation]
|
|
119
|
+
with pytest.raises(FileNotFoundError, match="metadata missing"):
|
|
120
|
+
getattr(jobs, operation)("missing", *args)
|
|
121
|
+
assert jobs.load_result("missing") is None
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from unittest.mock import Mock
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
SERVER_ROOT = Path(__file__).parents[3] / "server"
|
|
10
|
+
sys.path.insert(0, str(SERVER_ROOT))
|
|
11
|
+
|
|
12
|
+
from config.model_registry import ( # noqa: E402
|
|
13
|
+
ModelRegistry,
|
|
14
|
+
generated_local_model_id,
|
|
15
|
+
public_record,
|
|
16
|
+
resolve_local_source,
|
|
17
|
+
resolve_huggingface_source,
|
|
18
|
+
infer_source_mode,
|
|
19
|
+
require_api_key,
|
|
20
|
+
validate_local_model_id,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FakeRedis:
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self.hashes = {}
|
|
27
|
+
|
|
28
|
+
def hget(self, key, field):
|
|
29
|
+
return self.hashes.get(key, {}).get(field)
|
|
30
|
+
|
|
31
|
+
def hvals(self, key):
|
|
32
|
+
return list(self.hashes.get(key, {}).values())
|
|
33
|
+
|
|
34
|
+
def hset(self, key, field, value):
|
|
35
|
+
self.hashes.setdefault(key, {})[field] = value
|
|
36
|
+
|
|
37
|
+
def hsetnx(self, key, field, value):
|
|
38
|
+
values = self.hashes.setdefault(key, {})
|
|
39
|
+
if field in values:
|
|
40
|
+
return 0
|
|
41
|
+
values[field] = value
|
|
42
|
+
return 1
|
|
43
|
+
|
|
44
|
+
def hdel(self, key, field):
|
|
45
|
+
self.hashes.get(key, {}).pop(field, None)
|
|
46
|
+
|
|
47
|
+
def hgetall(self, key):
|
|
48
|
+
return dict(self.hashes.get(key, {}))
|
|
49
|
+
|
|
50
|
+
def setex(self, key, ttl, value):
|
|
51
|
+
self.hashes.setdefault("strings", {})[key] = value
|
|
52
|
+
|
|
53
|
+
def getdel(self, key):
|
|
54
|
+
return self.hashes.setdefault("strings", {}).pop(key, None)
|
|
55
|
+
|
|
56
|
+
def delete(self, key):
|
|
57
|
+
self.hashes.setdefault("strings", {}).pop(key, None)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def record(model_id="model", source="org/model"):
|
|
61
|
+
return {
|
|
62
|
+
"model_id": model_id,
|
|
63
|
+
"task_type": "classification",
|
|
64
|
+
"source_type": "huggingface",
|
|
65
|
+
"source": source,
|
|
66
|
+
"config_path": None,
|
|
67
|
+
"status": "ready",
|
|
68
|
+
"generation": 1,
|
|
69
|
+
"created_at": "now",
|
|
70
|
+
"updated_at": "now",
|
|
71
|
+
"error": None,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_registry_reserves_idempotently_and_rejects_conflicts():
|
|
76
|
+
registry = ModelRegistry(SimpleNamespace(redis_url="redis://unused"), FakeRedis())
|
|
77
|
+
_, created = registry.reserve(record())
|
|
78
|
+
assert created
|
|
79
|
+
existing, created = registry.reserve(record())
|
|
80
|
+
assert not created
|
|
81
|
+
assert existing["source"] == "org/model"
|
|
82
|
+
with pytest.raises(FileExistsError):
|
|
83
|
+
registry.reserve(record(source="other/model"))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_local_source_stays_inside_model_root(tmp_path):
|
|
87
|
+
root = tmp_path / "models"
|
|
88
|
+
folder = root / "custom"
|
|
89
|
+
folder.mkdir(parents=True)
|
|
90
|
+
(folder / "config.yaml").write_text("MODEL: {}")
|
|
91
|
+
settings = SimpleNamespace(model_root=root)
|
|
92
|
+
|
|
93
|
+
weights, config = resolve_local_source(settings, str(folder), None)
|
|
94
|
+
assert weights == folder
|
|
95
|
+
assert config == folder / "config.yaml"
|
|
96
|
+
assert generated_local_model_id(folder).startswith("local:custom-")
|
|
97
|
+
|
|
98
|
+
outside = tmp_path / "outside"
|
|
99
|
+
outside.mkdir()
|
|
100
|
+
with pytest.raises(ValueError, match="OSL_MODEL_ROOT"):
|
|
101
|
+
resolve_local_source(settings, str(outside), None)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_custom_id_and_public_local_record_validation():
|
|
105
|
+
assert validate_local_model_id("football-model:v2") == "football-model:v2"
|
|
106
|
+
with pytest.raises(ValueError):
|
|
107
|
+
validate_local_model_id("folder/model")
|
|
108
|
+
local = record()
|
|
109
|
+
local.update({"source_type": "local", "source": "/secret/model", "config_path": "/secret/config", "error": "at /secret/model"})
|
|
110
|
+
visible = public_record(local)
|
|
111
|
+
assert "source" not in visible
|
|
112
|
+
assert "config_path" not in visible
|
|
113
|
+
assert "/secret" not in visible["error"]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@pytest.mark.parametrize("runner_section", ["MODEL", "TRAIN"])
|
|
117
|
+
def test_config_driven_runner_is_weightless(tmp_path, runner_section):
|
|
118
|
+
config = tmp_path / "config.yaml"
|
|
119
|
+
config.write_text(f"{runner_section}:\n runner:\n type: runner_h5_header_rule\n")
|
|
120
|
+
assert infer_source_mode(str(config), "org/config-only") == "config_only"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_ordinary_config_requires_checkpoint(tmp_path):
|
|
124
|
+
config = tmp_path / "config.yaml"
|
|
125
|
+
config.write_text("TRAIN:\n runner:\n type: default\n")
|
|
126
|
+
assert infer_source_mode(str(config), "org/model") == "checkpoint"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_operation_secret_is_consumed_and_not_part_of_records():
|
|
130
|
+
settings = SimpleNamespace(redis_url="redis://unused", model_operation_timeout_seconds=60)
|
|
131
|
+
registry = ModelRegistry(settings, FakeRedis())
|
|
132
|
+
registry.store_operation_secret("operation", "hf_secret")
|
|
133
|
+
assert registry.consume_operation_secret("operation") == "hf_secret"
|
|
134
|
+
assert registry.consume_operation_secret("operation") is None
|
|
135
|
+
assert registry.list() == []
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _mock_hub(monkeypatch, config_path, files):
|
|
139
|
+
import huggingface_hub
|
|
140
|
+
|
|
141
|
+
download = Mock(return_value=str(config_path))
|
|
142
|
+
listing = Mock(return_value=files)
|
|
143
|
+
monkeypatch.setattr(huggingface_hub, "hf_hub_download", download)
|
|
144
|
+
monkeypatch.setattr(huggingface_hub, "list_repo_files", listing)
|
|
145
|
+
return download, listing
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@pytest.mark.parametrize("variant", [
|
|
149
|
+
"h5_header_skeleton", "h5_header_skeleton_recall", "h5_header_skeleton_max_recall",
|
|
150
|
+
])
|
|
151
|
+
def test_hf_rule_source_loads_localization_service_without_weights(tmp_path, monkeypatch, variant):
|
|
152
|
+
from opensportslib.core.config import load_config
|
|
153
|
+
from opensportslib.core.config.loader import save_config
|
|
154
|
+
from services.localization_service import LocalizationService
|
|
155
|
+
|
|
156
|
+
canonical = load_config(
|
|
157
|
+
SERVER_ROOT.parent / "opensportslib/configs/localization/h5_header_skeleton.yaml",
|
|
158
|
+
as_namespace=False,
|
|
159
|
+
)
|
|
160
|
+
canonical["MODEL"]["components"]["rule"]["source"]["name"] = variant
|
|
161
|
+
canonical["SYSTEM"]["paths"]["save_dir"] = str(tmp_path / "outputs")
|
|
162
|
+
canonical["DATA"]["common"]["data_root"] = str(tmp_path)
|
|
163
|
+
canonical["DATA"]["common"]["splits"]["test"]["source_path"] = str(tmp_path)
|
|
164
|
+
config_path = tmp_path / "config.yaml"
|
|
165
|
+
save_config(canonical, config_path)
|
|
166
|
+
download, listing = _mock_hub(monkeypatch, config_path, ["config.yaml"])
|
|
167
|
+
model_id = "example/skeleton"
|
|
168
|
+
|
|
169
|
+
resolved_path, source_mode, weights = resolve_huggingface_source(model_id, token="test-token")
|
|
170
|
+
assert (resolved_path, source_mode, weights) == (str(config_path), "config_only", None)
|
|
171
|
+
download.assert_called_once_with(repo_id=model_id, filename="config.yaml", token="test-token")
|
|
172
|
+
listing.assert_called_once_with(model_id, token="test-token")
|
|
173
|
+
|
|
174
|
+
registry = ModelRegistry(SimpleNamespace(redis_url="redis://unused"), FakeRedis())
|
|
175
|
+
registered = record(model_id=model_id, source=model_id)
|
|
176
|
+
registered.update(task_type="localization", config_path=resolved_path, source_mode=source_mode)
|
|
177
|
+
settings = registry.model_settings(registered)
|
|
178
|
+
assert settings.weights is None
|
|
179
|
+
service = LocalizationService(settings)
|
|
180
|
+
service.preload()
|
|
181
|
+
assert service.model.last_loaded_weights is None
|
|
182
|
+
assert service.model.config.MODEL.components.rule.source.name == variant
|
|
183
|
+
assert service.label_schema["action"]["labels"] == ["header"]
|
|
184
|
+
# Older persisted records without source_mode must infer the same behavior.
|
|
185
|
+
registered.pop("source_mode")
|
|
186
|
+
assert registry.model_settings(registered).weights is None
|
|
187
|
+
assert download.call_count == 1
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@pytest.mark.parametrize("checkpoint", ["model.pt", "model.pth.tar", "adapter_model.safetensors"])
|
|
191
|
+
def test_hf_checkpoint_source_keeps_weights(tmp_path, monkeypatch, checkpoint):
|
|
192
|
+
config_path = tmp_path / "config.yaml"
|
|
193
|
+
config_path.write_text("MODEL:\n runner:\n type: runner_e2e\n")
|
|
194
|
+
_mock_hub(monkeypatch, config_path, ["config.yaml", checkpoint])
|
|
195
|
+
model_id = "example/checkpoint"
|
|
196
|
+
|
|
197
|
+
path, mode, weights = resolve_huggingface_source(model_id)
|
|
198
|
+
assert (path, mode, weights) == (str(config_path), "checkpoint", model_id)
|
|
199
|
+
registry = ModelRegistry(SimpleNamespace(redis_url="redis://unused"), FakeRedis())
|
|
200
|
+
registered = record(model_id=model_id, source=model_id)
|
|
201
|
+
registered.update(config_path=path, source_mode=mode)
|
|
202
|
+
assert registry.model_settings(registered).weights == model_id
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@pytest.mark.parametrize("content", [
|
|
206
|
+
"MODEL:\n runner:\n type: runner_e2e\n",
|
|
207
|
+
"{}",
|
|
208
|
+
"[]",
|
|
209
|
+
"MODEL: [",
|
|
210
|
+
])
|
|
211
|
+
def test_hf_weightless_source_without_supported_runner_is_rejected(tmp_path, monkeypatch, content):
|
|
212
|
+
config_path = tmp_path / "config.yaml"
|
|
213
|
+
config_path.write_text(content)
|
|
214
|
+
_mock_hub(monkeypatch, config_path, ["config.yaml"])
|
|
215
|
+
|
|
216
|
+
with pytest.raises(ValueError, match="no supported checkpoint"):
|
|
217
|
+
resolve_huggingface_source("example/unsupported")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_hf_repository_access_failure_is_reported(tmp_path, monkeypatch):
|
|
221
|
+
config_path = tmp_path / "config.yaml"
|
|
222
|
+
config_path.write_text("MODEL: {}")
|
|
223
|
+
_, listing = _mock_hub(monkeypatch, config_path, [])
|
|
224
|
+
listing.side_effect = RuntimeError("repository access denied")
|
|
225
|
+
with pytest.raises(ValueError, match="Could not access Hugging Face repository"):
|
|
226
|
+
resolve_huggingface_source("example/gated", token="test-token")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_default_selection_and_deletion_preserve_other_tasks():
|
|
230
|
+
registry = ModelRegistry(SimpleNamespace(redis_url="redis://unused"), FakeRedis())
|
|
231
|
+
registry.put(record("classifier"))
|
|
232
|
+
localizer = record("localizer")
|
|
233
|
+
localizer["task_type"] = "localization"
|
|
234
|
+
registry.put(localizer)
|
|
235
|
+
registry.set_default("classification", "classifier")
|
|
236
|
+
registry.set_default("localization", "localizer")
|
|
237
|
+
assert registry.resolve("classification", None)["model_id"] == "classifier"
|
|
238
|
+
assert registry.resolve("localization", None)["model_id"] == "localizer"
|
|
239
|
+
assert [item["model_id"] for item in registry.list()] == ["classifier", "localizer"]
|
|
240
|
+
registry.delete("classifier")
|
|
241
|
+
assert registry.resolve("classification", None) is None
|
|
242
|
+
assert registry.resolve("localization", None)["model_id"] == "localizer"
|
|
243
|
+
assert registry.get("classifier") is None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@pytest.mark.parametrize("status", ["registering", "failed", "unregistering"])
|
|
247
|
+
def test_only_ready_models_can_be_defaults(status):
|
|
248
|
+
registry = ModelRegistry(SimpleNamespace(redis_url="redis://unused"), FakeRedis())
|
|
249
|
+
candidate = record()
|
|
250
|
+
candidate["status"] = status
|
|
251
|
+
registry.put(candidate)
|
|
252
|
+
with pytest.raises(ValueError, match="READY"):
|
|
253
|
+
registry.set_default("classification", "model")
|
|
254
|
+
assert registry.resolve("classification", None) is None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_unknown_and_wrong_task_models_cannot_be_selected_as_defaults():
|
|
258
|
+
registry = ModelRegistry(SimpleNamespace(redis_url="redis://unused"), FakeRedis())
|
|
259
|
+
with pytest.raises(KeyError):
|
|
260
|
+
registry.set_default("classification", "missing")
|
|
261
|
+
registry.put(record())
|
|
262
|
+
with pytest.raises(ValueError, match="requested task"):
|
|
263
|
+
registry.set_default("localization", "model")
|
|
264
|
+
with pytest.raises(TypeError, match="not `localization`"):
|
|
265
|
+
registry.resolve("localization", "model")
|
|
266
|
+
assert registry.resolve("classification", "missing") is None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_operations_round_trip_and_secrets_can_be_removed():
|
|
270
|
+
registry = ModelRegistry(SimpleNamespace(redis_url="redis://unused", model_operation_timeout_seconds=60), FakeRedis())
|
|
271
|
+
assert registry.get_operation("missing") is None
|
|
272
|
+
operation = registry.create_operation("register", "model")
|
|
273
|
+
assert operation["status"] == "queued"
|
|
274
|
+
operation.update(status="failed", error="missing config")
|
|
275
|
+
registry.save_operation(operation)
|
|
276
|
+
assert registry.get_operation(operation["operation_id"])["error"] == "missing config"
|
|
277
|
+
registry.store_operation_secret(operation["operation_id"], "temporary")
|
|
278
|
+
registry.delete_operation_secret(operation["operation_id"])
|
|
279
|
+
assert registry.consume_operation_secret(operation["operation_id"]) is None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@pytest.mark.parametrize("authorization", [None, "Bearer wrong", "Bearer"])
|
|
283
|
+
def test_api_key_rejects_missing_or_wrong_credentials(authorization):
|
|
284
|
+
with pytest.raises(PermissionError, match="Invalid API key"):
|
|
285
|
+
require_api_key(SimpleNamespace(api_key="expected"), authorization)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_api_key_accepts_valid_bearer_and_disables_unconfigured_admin():
|
|
289
|
+
require_api_key(SimpleNamespace(api_key="expected"), "Bearer expected")
|
|
290
|
+
with pytest.raises(RuntimeError, match="disabled"):
|
|
291
|
+
require_api_key(SimpleNamespace(api_key=""), "Bearer expected")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def test_local_source_rejects_symlink_escape_and_requires_file_config(tmp_path):
|
|
295
|
+
root = tmp_path / "models"
|
|
296
|
+
root.mkdir()
|
|
297
|
+
outside = tmp_path / "outside.pt"
|
|
298
|
+
outside.write_bytes(b"checkpoint")
|
|
299
|
+
escaped = root / "escape.pt"
|
|
300
|
+
escaped.symlink_to(outside)
|
|
301
|
+
settings = SimpleNamespace(model_root=root)
|
|
302
|
+
with pytest.raises(ValueError, match="OSL_MODEL_ROOT"):
|
|
303
|
+
resolve_local_source(settings, str(escaped), None)
|
|
304
|
+
checkpoint = root / "model.pt"
|
|
305
|
+
checkpoint.write_bytes(b"checkpoint")
|
|
306
|
+
with pytest.raises(ValueError, match="config_path is required"):
|
|
307
|
+
resolve_local_source(settings, str(checkpoint), None)
|
|
308
|
+
with pytest.raises(ValueError, match="config does not exist"):
|
|
309
|
+
resolve_local_source(settings, str(checkpoint), str(root / "missing.yaml"))
|
|
310
|
+
with pytest.raises(ValueError, match="weights do not exist"):
|
|
311
|
+
resolve_local_source(settings, str(root / "missing.pt"), None)
|
|
312
|
+
config = root / "config.yaml"
|
|
313
|
+
config.write_text("MODEL: {}")
|
|
314
|
+
assert resolve_local_source(settings, str(checkpoint), str(config)) == (checkpoint, config)
|