1import json 2import os 3 4from enum import Enum 5from typing import Any, Dict, List, NamedTuple, Optional, Tuple 6 7 8JSON = Dict[str, Any] 9 10 11DEFAULT_MAP_FILE = "projects.json" 12 13 14class DownloadType(str, Enum): 15 GIT = "git" 16 ZIP = "zip" 17 SCRIPT = "script" 18 19 20class ProjectInfo(NamedTuple): 21 """ 22 Information about a project to analyze. 23 """ 24 name: str 25 mode: int 26 source: DownloadType = DownloadType.SCRIPT 27 origin: str = "" 28 commit: str = "" 29 enabled: bool = True 30 31 def with_fields(self, **kwargs) -> "ProjectInfo": 32 """ 33 Create a copy of this project info with customized fields. 34 NamedTuple is immutable and this is a way to create modified copies. 35 36 info.enabled = True 37 info.mode = 1 38 39 can be done as follows: 40 41 modified = info.with_fields(enbled=True, mode=1) 42 """ 43 return ProjectInfo(**{**self._asdict(), **kwargs}) 44 45 46class ProjectMap: 47 """ 48 Project map stores info about all the "registered" projects. 49 """ 50 def __init__(self, path: Optional[str] = None, should_exist: bool = True): 51 """ 52 :param path: optional path to a project JSON file, when None defaults 53 to DEFAULT_MAP_FILE. 54 :param should_exist: flag to tell if it's an exceptional situation when 55 the project file doesn't exist, creates an empty 56 project list instead if we are not expecting it to 57 exist. 58 """ 59 if path is None: 60 path = os.path.join(os.path.abspath(os.curdir), DEFAULT_MAP_FILE) 61 62 if not os.path.exists(path): 63 if should_exist: 64 raise ValueError( 65 f"Cannot find the project map file {path}" 66 f"\nRunning script for the wrong directory?\n") 67 else: 68 self._create_empty(path) 69 70 self.path = path 71 self._load_projects() 72 73 def save(self): 74 """ 75 Save project map back to its original file. 76 """ 77 self._save(self.projects, self.path) 78 79 def _load_projects(self): 80 with open(self.path) as raw_data: 81 raw_projects = json.load(raw_data) 82 83 if not isinstance(raw_projects, list): 84 raise ValueError( 85 "Project map should be a list of JSON objects") 86 87 self.projects = self._parse(raw_projects) 88 89 @staticmethod 90 def _parse(raw_projects: List[JSON]) -> List[ProjectInfo]: 91 return [ProjectMap._parse_project(raw_project) 92 for raw_project in raw_projects] 93 94 @staticmethod 95 def _parse_project(raw_project: JSON) -> ProjectInfo: 96 try: 97 name: str = raw_project["name"] 98 build_mode: int = raw_project["mode"] 99 enabled: bool = raw_project.get("enabled", True) 100 source: DownloadType = raw_project.get("source", "zip") 101 102 if source == DownloadType.GIT: 103 origin, commit = ProjectMap._get_git_params(raw_project) 104 else: 105 origin, commit = "", "" 106 107 return ProjectInfo(name, build_mode, source, origin, commit, 108 enabled) 109 110 except KeyError as e: 111 raise ValueError( 112 f"Project info is required to have a '{e.args[0]}' field") 113 114 @staticmethod 115 def _get_git_params(raw_project: JSON) -> Tuple[str, str]: 116 try: 117 return raw_project["origin"], raw_project["commit"] 118 except KeyError as e: 119 raise ValueError( 120 f"Profect info is required to have a '{e.args[0]}' field " 121 f"if it has a 'git' source") 122 123 @staticmethod 124 def _create_empty(path: str): 125 ProjectMap._save([], path) 126 127 @staticmethod 128 def _save(projects: List[ProjectInfo], path: str): 129 with open(path, "w") as output: 130 json.dump(ProjectMap._convert_infos_to_dicts(projects), 131 output, indent=2) 132 133 @staticmethod 134 def _convert_infos_to_dicts(projects: List[ProjectInfo]) -> List[JSON]: 135 return [ProjectMap._convert_info_to_dict(project) 136 for project in projects] 137 138 @staticmethod 139 def _convert_info_to_dict(project: ProjectInfo) -> JSON: 140 whole_dict = project._asdict() 141 defaults = project._field_defaults 142 143 # there is no need in serializing fields with default values 144 for field, default_value in defaults.items(): 145 if whole_dict[field] == default_value: 146 del whole_dict[field] 147 148 return whole_dict 149