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