manager.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. import logging
  2. from pathlib import Path
  3. import ap_git
  4. from utils import TaskRunner
  5. from metadata_manager.versions_manager.models import RemoteInfo, VersionInfo
  6. from metadata_manager.versions_manager.providers import (
  7. VersionsProvider,
  8. build_default_providers,
  9. )
  10. from metadata_manager.vehicles_manager import VehiclesManager as vehm
  11. REMOTES_SCHEMA_PATH = Path(__file__).resolve().parent.parent / "remotes.schema.json"
  12. def _release_type_dedup_priority(release_type: str) -> int:
  13. """Lower value wins when multiple releases collide on a dedup key."""
  14. match release_type:
  15. case "stable":
  16. return 0
  17. case "beta":
  18. return 1
  19. case "latest":
  20. return 2
  21. case "tag":
  22. return 3
  23. case _:
  24. return 99
  25. def _version_dedup_key(version: VersionInfo) -> str:
  26. """
  27. Collapse releases that share a real version number (e.g. stable+beta 4.7.0).
  28. Placeholder numbers like "NA" (latest/dev) keep identity by version_id so
  29. unrelated entries are not merged.
  30. """
  31. number = (version.version_number or "").strip()
  32. if number and number.upper() != "NA":
  33. return f"num:{number}"
  34. return f"id:{version.version_id}"
  35. class VersionsManager:
  36. """
  37. Version manager merges version metadata from different pluggable providers.
  38. """
  39. __singleton = None
  40. def __init__(
  41. self,
  42. ap_repo: ap_git.GitRepo,
  43. remotes_json_path: str,
  44. manifest_json=None,
  45. providers: list[VersionsProvider] | None = None,
  46. ):
  47. if vehm.get_singleton() is None:
  48. raise RuntimeError("VehiclesManager should be initialised first")
  49. if VersionsManager.__singleton:
  50. raise RuntimeError("VersionsManager must be a singleton.")
  51. self.logger = logging.getLogger(__name__)
  52. if providers is None:
  53. if manifest_json is None:
  54. raise ValueError("manifest_json is required when providers is omitted")
  55. providers = build_default_providers(
  56. manifest_json=manifest_json,
  57. remotes_json_path=remotes_json_path,
  58. schema_path=str(REMOTES_SCHEMA_PATH),
  59. )
  60. self._providers = providers
  61. self.repo = ap_repo
  62. self.__task__runner = TaskRunner(tasks=((self.refresh_all, 1200),))
  63. VersionsManager.__singleton = self
  64. def start(self) -> None:
  65. self.logger.info("Starting VersionsManager background refresh jobs.")
  66. self.__task__runner.start()
  67. def stop(self) -> None:
  68. self.logger.info("Stopping VersionsManager background refresh jobs.")
  69. self.__task__runner.stop()
  70. def refresh_all(self) -> None:
  71. for provider in self._providers:
  72. try:
  73. provider.refresh()
  74. except Exception as exc:
  75. self.logger.error(
  76. "Provider %s refresh failed: %s", provider.name, exc
  77. )
  78. self._sync_remotes_with_ap_repo()
  79. def get_all_remotes_info(self) -> list[RemoteInfo]:
  80. remotes = {}
  81. for provider in self._providers:
  82. for remote in provider.get_remotes():
  83. remotes[remote.name] = remote
  84. return list(remotes.values())
  85. def get_remote_info(self, remote_name: str) -> RemoteInfo | None:
  86. return next(
  87. (remote for remote in self.get_all_remotes_info()
  88. if remote.name == remote_name),
  89. None,
  90. )
  91. def get_versions_for_vehicle(self, vehicle_id: str) -> list[VersionInfo]:
  92. if vehicle_id is None:
  93. raise ValueError("Vehicle ID is a required parameter.")
  94. vehicle = vehm.get_singleton().get_vehicle_by_id(vehicle_id)
  95. if vehicle is None:
  96. raise ValueError(f"Invalid vehicle ID '{vehicle_id}'.")
  97. by_key: dict[str, VersionInfo] = {}
  98. for provider in self._providers:
  99. for version in provider.get_versions(vehicle_id):
  100. key = _version_dedup_key(version)
  101. existing = by_key.get(key)
  102. if existing is None:
  103. by_key[key] = version
  104. continue
  105. if _release_type_dedup_priority(version.release_type) < (
  106. _release_type_dedup_priority(existing.release_type)
  107. ):
  108. by_key[key] = version
  109. return list(by_key.values())
  110. def is_version_listed(self, vehicle_id: str, version_id: str) -> bool:
  111. if vehicle_id is None:
  112. raise ValueError("vehicle_id is a required parameter.")
  113. if version_id is None:
  114. raise ValueError("version_id is a required parameter.")
  115. return version_id in [
  116. version.version_id
  117. for version in self.get_versions_for_vehicle(vehicle_id=vehicle_id)
  118. ]
  119. def get_version_info(self, vehicle_id: str, version_id: str) -> VersionInfo | None:
  120. return next(
  121. (
  122. version
  123. for version in self.get_versions_for_vehicle(vehicle_id=vehicle_id)
  124. if version.version_id == version_id
  125. ),
  126. None,
  127. )
  128. def _sync_remotes_with_ap_repo(self) -> None:
  129. remotes = tuple(
  130. (remote.name, remote.url)
  131. for remote in self.get_all_remotes_info()
  132. )
  133. if remotes:
  134. self.repo.remote_add_bulk(remotes=remotes, force=True)
  135. @staticmethod
  136. def get_singleton():
  137. return VersionsManager.__singleton