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