manager.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  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._remotes_json_path = remotes_json_path
  63. self.__task__runner = TaskRunner(tasks=((self.refresh_all, 1200),))
  64. VersionsManager.__singleton = self
  65. def start(self) -> None:
  66. self.logger.info("Starting VersionsManager background refresh jobs.")
  67. self.__task__runner.start()
  68. def stop(self) -> None:
  69. self.logger.info("Stopping VersionsManager background refresh jobs.")
  70. self.__task__runner.stop()
  71. def refresh_all(self) -> None:
  72. for provider in self._providers:
  73. try:
  74. provider.refresh()
  75. except Exception as exc:
  76. self.logger.error(
  77. "Provider %s refresh failed: %s", provider.name, exc
  78. )
  79. self._sync_remotes_with_ap_repo()
  80. def get_all_remotes_info(self) -> list[RemoteInfo]:
  81. remotes = {}
  82. for provider in self._providers:
  83. for remote in provider.get_remotes():
  84. remotes[remote.name] = remote
  85. return list(remotes.values())
  86. def get_remote_info(self, remote_name: str) -> RemoteInfo | None:
  87. return next(
  88. (remote for remote in self.get_all_remotes_info()
  89. if remote.name == remote_name),
  90. None,
  91. )
  92. def get_versions_for_vehicle(self, vehicle_id: str) -> list[VersionInfo]:
  93. if vehicle_id is None:
  94. raise ValueError("Vehicle ID is a required parameter.")
  95. vehicle = vehm.get_singleton().get_vehicle_by_id(vehicle_id)
  96. if vehicle is None:
  97. raise ValueError(f"Invalid vehicle ID '{vehicle_id}'.")
  98. by_key: dict[str, VersionInfo] = {}
  99. for provider in self._providers:
  100. for version in provider.get_versions(vehicle_id):
  101. key = _version_dedup_key(version)
  102. existing = by_key.get(key)
  103. if existing is None:
  104. by_key[key] = version
  105. continue
  106. if _release_type_dedup_priority(version.release_type) < (
  107. _release_type_dedup_priority(existing.release_type)
  108. ):
  109. by_key[key] = version
  110. return list(by_key.values())
  111. def is_version_listed(self, vehicle_id: str, version_id: str) -> bool:
  112. if vehicle_id is None:
  113. raise ValueError("vehicle_id is a required parameter.")
  114. if version_id is None:
  115. raise ValueError("version_id is a required parameter.")
  116. return version_id in [
  117. version.version_id
  118. for version in self.get_versions_for_vehicle(vehicle_id=vehicle_id)
  119. ]
  120. def get_version_info(self, vehicle_id: str, version_id: str) -> VersionInfo | None:
  121. return next(
  122. (
  123. version
  124. for version in self.get_versions_for_vehicle(vehicle_id=vehicle_id)
  125. if version.version_id == version_id
  126. ),
  127. None,
  128. )
  129. def _sync_remotes_with_ap_repo(self) -> None:
  130. remotes = tuple(
  131. (remote.name, remote.url)
  132. for remote in self.get_all_remotes_info()
  133. )
  134. if remotes:
  135. self.repo.remote_add_bulk(remotes=remotes, force=True)
  136. @staticmethod
  137. def get_singleton():
  138. return VersionsManager.__singleton