1ec727ea7Spatrickimport json 2ec727ea7Spatrickimport os 3ec727ea7Spatrick 4*a9ac8606Spatrickfrom enum import auto, Enum 5ec727ea7Spatrickfrom typing import Any, Dict, List, NamedTuple, Optional, Tuple 6ec727ea7Spatrick 7ec727ea7Spatrick 8ec727ea7SpatrickJSON = Dict[str, Any] 9ec727ea7Spatrick 10ec727ea7Spatrick 11ec727ea7SpatrickDEFAULT_MAP_FILE = "projects.json" 12ec727ea7Spatrick 13ec727ea7Spatrick 14ec727ea7Spatrickclass DownloadType(str, Enum): 15ec727ea7Spatrick GIT = "git" 16ec727ea7Spatrick ZIP = "zip" 17ec727ea7Spatrick SCRIPT = "script" 18ec727ea7Spatrick 19ec727ea7Spatrick 20*a9ac8606Spatrickclass Size(int, Enum): 21*a9ac8606Spatrick """ 22*a9ac8606Spatrick Size of the project. 23*a9ac8606Spatrick 24*a9ac8606Spatrick Sizes do not directly correspond to the number of lines or files in the 25*a9ac8606Spatrick project. The key factor that is important for the developers of the 26*a9ac8606Spatrick analyzer is the time it takes to analyze the project. Here is how 27*a9ac8606Spatrick the following sizes map to times: 28*a9ac8606Spatrick 29*a9ac8606Spatrick TINY: <1min 30*a9ac8606Spatrick SMALL: 1min-10min 31*a9ac8606Spatrick BIG: 10min-1h 32*a9ac8606Spatrick HUGE: >1h 33*a9ac8606Spatrick 34*a9ac8606Spatrick The borders are a bit of a blur, especially because analysis time varies 35*a9ac8606Spatrick from one machine to another. However, the relative times will stay pretty 36*a9ac8606Spatrick similar, and these groupings will still be helpful. 37*a9ac8606Spatrick 38*a9ac8606Spatrick UNSPECIFIED is a very special case, which is intentionally last in the list 39*a9ac8606Spatrick of possible sizes. If the user wants to filter projects by one of the 40*a9ac8606Spatrick possible sizes, we want projects with UNSPECIFIED size to be filtered out 41*a9ac8606Spatrick for any given size. 42*a9ac8606Spatrick """ 43*a9ac8606Spatrick TINY = auto() 44*a9ac8606Spatrick SMALL = auto() 45*a9ac8606Spatrick BIG = auto() 46*a9ac8606Spatrick HUGE = auto() 47*a9ac8606Spatrick UNSPECIFIED = auto() 48*a9ac8606Spatrick 49*a9ac8606Spatrick @staticmethod 50*a9ac8606Spatrick def from_str(raw_size: Optional[str]) -> "Size": 51*a9ac8606Spatrick """ 52*a9ac8606Spatrick Construct a Size object from an optional string. 53*a9ac8606Spatrick 54*a9ac8606Spatrick :param raw_size: optional string representation of the desired Size 55*a9ac8606Spatrick object. None will produce UNSPECIFIED size. 56*a9ac8606Spatrick 57*a9ac8606Spatrick This method is case-insensitive, so raw sizes 'tiny', 'TINY', and 58*a9ac8606Spatrick 'TiNy' will produce the same result. 59*a9ac8606Spatrick """ 60*a9ac8606Spatrick if raw_size is None: 61*a9ac8606Spatrick return Size.UNSPECIFIED 62*a9ac8606Spatrick 63*a9ac8606Spatrick raw_size_upper = raw_size.upper() 64*a9ac8606Spatrick # The implementation is decoupled from the actual values of the enum, 65*a9ac8606Spatrick # so we can easily add or modify it without bothering about this 66*a9ac8606Spatrick # function. 67*a9ac8606Spatrick for possible_size in Size: 68*a9ac8606Spatrick if possible_size.name == raw_size_upper: 69*a9ac8606Spatrick return possible_size 70*a9ac8606Spatrick 71*a9ac8606Spatrick possible_sizes = [size.name.lower() for size in Size 72*a9ac8606Spatrick # no need in showing our users this size 73*a9ac8606Spatrick if size != Size.UNSPECIFIED] 74*a9ac8606Spatrick raise ValueError(f"Incorrect project size '{raw_size}'. " 75*a9ac8606Spatrick f"Available sizes are {possible_sizes}") 76*a9ac8606Spatrick 77*a9ac8606Spatrick 78ec727ea7Spatrickclass ProjectInfo(NamedTuple): 79ec727ea7Spatrick """ 80ec727ea7Spatrick Information about a project to analyze. 81ec727ea7Spatrick """ 82ec727ea7Spatrick name: str 83ec727ea7Spatrick mode: int 84ec727ea7Spatrick source: DownloadType = DownloadType.SCRIPT 85ec727ea7Spatrick origin: str = "" 86ec727ea7Spatrick commit: str = "" 87ec727ea7Spatrick enabled: bool = True 88*a9ac8606Spatrick size: Size = Size.UNSPECIFIED 89ec727ea7Spatrick 90ec727ea7Spatrick def with_fields(self, **kwargs) -> "ProjectInfo": 91ec727ea7Spatrick """ 92ec727ea7Spatrick Create a copy of this project info with customized fields. 93ec727ea7Spatrick NamedTuple is immutable and this is a way to create modified copies. 94ec727ea7Spatrick 95ec727ea7Spatrick info.enabled = True 96ec727ea7Spatrick info.mode = 1 97ec727ea7Spatrick 98ec727ea7Spatrick can be done as follows: 99ec727ea7Spatrick 100ec727ea7Spatrick modified = info.with_fields(enbled=True, mode=1) 101ec727ea7Spatrick """ 102ec727ea7Spatrick return ProjectInfo(**{**self._asdict(), **kwargs}) 103ec727ea7Spatrick 104ec727ea7Spatrick 105ec727ea7Spatrickclass ProjectMap: 106ec727ea7Spatrick """ 107ec727ea7Spatrick Project map stores info about all the "registered" projects. 108ec727ea7Spatrick """ 109ec727ea7Spatrick def __init__(self, path: Optional[str] = None, should_exist: bool = True): 110ec727ea7Spatrick """ 111ec727ea7Spatrick :param path: optional path to a project JSON file, when None defaults 112ec727ea7Spatrick to DEFAULT_MAP_FILE. 113ec727ea7Spatrick :param should_exist: flag to tell if it's an exceptional situation when 114ec727ea7Spatrick the project file doesn't exist, creates an empty 115ec727ea7Spatrick project list instead if we are not expecting it to 116ec727ea7Spatrick exist. 117ec727ea7Spatrick """ 118ec727ea7Spatrick if path is None: 119ec727ea7Spatrick path = os.path.join(os.path.abspath(os.curdir), DEFAULT_MAP_FILE) 120ec727ea7Spatrick 121ec727ea7Spatrick if not os.path.exists(path): 122ec727ea7Spatrick if should_exist: 123ec727ea7Spatrick raise ValueError( 124ec727ea7Spatrick f"Cannot find the project map file {path}" 125ec727ea7Spatrick f"\nRunning script for the wrong directory?\n") 126ec727ea7Spatrick else: 127ec727ea7Spatrick self._create_empty(path) 128ec727ea7Spatrick 129ec727ea7Spatrick self.path = path 130ec727ea7Spatrick self._load_projects() 131ec727ea7Spatrick 132ec727ea7Spatrick def save(self): 133ec727ea7Spatrick """ 134ec727ea7Spatrick Save project map back to its original file. 135ec727ea7Spatrick """ 136ec727ea7Spatrick self._save(self.projects, self.path) 137ec727ea7Spatrick 138ec727ea7Spatrick def _load_projects(self): 139ec727ea7Spatrick with open(self.path) as raw_data: 140ec727ea7Spatrick raw_projects = json.load(raw_data) 141ec727ea7Spatrick 142ec727ea7Spatrick if not isinstance(raw_projects, list): 143ec727ea7Spatrick raise ValueError( 144ec727ea7Spatrick "Project map should be a list of JSON objects") 145ec727ea7Spatrick 146ec727ea7Spatrick self.projects = self._parse(raw_projects) 147ec727ea7Spatrick 148ec727ea7Spatrick @staticmethod 149ec727ea7Spatrick def _parse(raw_projects: List[JSON]) -> List[ProjectInfo]: 150ec727ea7Spatrick return [ProjectMap._parse_project(raw_project) 151ec727ea7Spatrick for raw_project in raw_projects] 152ec727ea7Spatrick 153ec727ea7Spatrick @staticmethod 154ec727ea7Spatrick def _parse_project(raw_project: JSON) -> ProjectInfo: 155ec727ea7Spatrick try: 156ec727ea7Spatrick name: str = raw_project["name"] 157ec727ea7Spatrick build_mode: int = raw_project["mode"] 158ec727ea7Spatrick enabled: bool = raw_project.get("enabled", True) 159ec727ea7Spatrick source: DownloadType = raw_project.get("source", "zip") 160*a9ac8606Spatrick size = Size.from_str(raw_project.get("size", None)) 161ec727ea7Spatrick 162ec727ea7Spatrick if source == DownloadType.GIT: 163ec727ea7Spatrick origin, commit = ProjectMap._get_git_params(raw_project) 164ec727ea7Spatrick else: 165ec727ea7Spatrick origin, commit = "", "" 166ec727ea7Spatrick 167ec727ea7Spatrick return ProjectInfo(name, build_mode, source, origin, commit, 168*a9ac8606Spatrick enabled, size) 169ec727ea7Spatrick 170ec727ea7Spatrick except KeyError as e: 171ec727ea7Spatrick raise ValueError( 172ec727ea7Spatrick f"Project info is required to have a '{e.args[0]}' field") 173ec727ea7Spatrick 174ec727ea7Spatrick @staticmethod 175ec727ea7Spatrick def _get_git_params(raw_project: JSON) -> Tuple[str, str]: 176ec727ea7Spatrick try: 177ec727ea7Spatrick return raw_project["origin"], raw_project["commit"] 178ec727ea7Spatrick except KeyError as e: 179ec727ea7Spatrick raise ValueError( 180ec727ea7Spatrick f"Profect info is required to have a '{e.args[0]}' field " 181ec727ea7Spatrick f"if it has a 'git' source") 182ec727ea7Spatrick 183ec727ea7Spatrick @staticmethod 184ec727ea7Spatrick def _create_empty(path: str): 185ec727ea7Spatrick ProjectMap._save([], path) 186ec727ea7Spatrick 187ec727ea7Spatrick @staticmethod 188ec727ea7Spatrick def _save(projects: List[ProjectInfo], path: str): 189ec727ea7Spatrick with open(path, "w") as output: 190ec727ea7Spatrick json.dump(ProjectMap._convert_infos_to_dicts(projects), 191ec727ea7Spatrick output, indent=2) 192ec727ea7Spatrick 193ec727ea7Spatrick @staticmethod 194ec727ea7Spatrick def _convert_infos_to_dicts(projects: List[ProjectInfo]) -> List[JSON]: 195ec727ea7Spatrick return [ProjectMap._convert_info_to_dict(project) 196ec727ea7Spatrick for project in projects] 197ec727ea7Spatrick 198ec727ea7Spatrick @staticmethod 199ec727ea7Spatrick def _convert_info_to_dict(project: ProjectInfo) -> JSON: 200ec727ea7Spatrick whole_dict = project._asdict() 201ec727ea7Spatrick defaults = project._field_defaults 202ec727ea7Spatrick 203ec727ea7Spatrick # there is no need in serializing fields with default values 204ec727ea7Spatrick for field, default_value in defaults.items(): 205ec727ea7Spatrick if whole_dict[field] == default_value: 206ec727ea7Spatrick del whole_dict[field] 207ec727ea7Spatrick 208ec727ea7Spatrick return whole_dict 209