|
| 1 | +"""Helpers for creating and updating projects""" |
| 2 | + |
| 3 | +import json |
| 4 | +import shutil |
| 5 | +from dataclasses import dataclass, field |
| 6 | +from pathlib import Path |
| 7 | +from typing import Any |
| 8 | + |
| 9 | +from packaging import version as p_version |
| 10 | + |
| 11 | +V3_BACKUP_PATH = "v3_to_v4_upgrade_backups" |
| 12 | +V4_BUILD = "szBuildVersion.json" |
| 13 | +SZ_SYS_PATH = Path("/opt/senzing") |
| 14 | +V4_SYS_PATH = SZ_SYS_PATH / "er" |
| 15 | +V4_DATA_PATH = SZ_SYS_PATH / "data" |
| 16 | +V4_SYS_BUILD = V4_SYS_PATH / V4_BUILD |
| 17 | + |
| 18 | +COPY_TO_PROJ = { |
| 19 | + "er/": {"files": ["*"], "excludes": ["sz_create_project", "sz_update_project", "_project_helpers.py"]}, |
| 20 | + "data": {"files": ["*"], "excludes": []}, |
| 21 | +} |
| 22 | + |
| 23 | +PERMISSIONS = { |
| 24 | + ".": { |
| 25 | + "dir_pint": 0, |
| 26 | + "file_pint": 0o660, |
| 27 | + "files": ["LICENSE", "NOTICES", "README.1ST", "szBuildVersion.json"], |
| 28 | + "excludes": ["setupEnv"], |
| 29 | + "recursive": False, |
| 30 | + }, |
| 31 | + "setupEnv": {"dir_pint": 0, "file_pint": 0o770, "files": [], "excludes": [], "recursive": False}, |
| 32 | + "bin": { |
| 33 | + "dir_pint": 0o770, |
| 34 | + "file_pint": 0o770, |
| 35 | + "files": ["*"], |
| 36 | + "excludes": ["__pycache__", "_sz_database.py", "_tool_helpers.py"], |
| 37 | + "recursive": False, |
| 38 | + }, |
| 39 | + "data": {"dir_pint": 0o770, "file_pint": 0o660, "files": ["*"], "excludes": [], "recursive": True}, |
| 40 | + "lib": {"dir_pint": 0o770, "file_pint": 0o660, "files": ["*"], "excludes": [], "recursive": False}, |
| 41 | + "resources": {"dir_pint": 0o770, "file_pint": 0o660, "files": ["*"], "excludes": ["setupEnv"], "recursive": True}, |
| 42 | + "resources/templates/setupEnv": { |
| 43 | + "dir_pint": 0, |
| 44 | + "file_pint": 0o770, |
| 45 | + "files": [], |
| 46 | + "excludes": [], |
| 47 | + "recursive": False, |
| 48 | + }, |
| 49 | + "sdk": {"dir_pint": 0o770, "file_pint": 0o660, "files": ["*"], "excludes": [], "recursive": True}, |
| 50 | + V3_BACKUP_PATH: {"dir_pint": 0o770, "file_pint": 0, "files": [], "excludes": [], "recursive": False}, |
| 51 | +} |
| 52 | + |
| 53 | +PERMISSIONS_2 = { |
| 54 | + "bin": { |
| 55 | + "dir_pint": 0o770, |
| 56 | + "file_pint": 0o660, |
| 57 | + "files": ["_sz_database.py", "_tool_helpers.py"], |
| 58 | + "excludes": [], |
| 59 | + "recursive": False, |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | + |
| 64 | +@dataclass() |
| 65 | +class SzBuildDetails: |
| 66 | + """Build information for a project or Senzing SDK system install""" |
| 67 | + |
| 68 | + platform: str |
| 69 | + version: str |
| 70 | + build_version: str |
| 71 | + build_number: str |
| 72 | + major: int = field(init=False) |
| 73 | + minor: int = field(init=False) |
| 74 | + micro: int = field(init=False) |
| 75 | + |
| 76 | + def __post_init__(self) -> None: |
| 77 | + self.version_parsed = p_version.parse(self.version) |
| 78 | + self.build_version_parsed = p_version.parse(self.build_version) |
| 79 | + self.major = self.version_parsed.major |
| 80 | + self.minor = self.version_parsed.minor |
| 81 | + self.micro = self.version_parsed.micro |
| 82 | + |
| 83 | + |
| 84 | +def get_build_details(path: Path) -> SzBuildDetails: |
| 85 | + """Return dataclass with the details from a build file.""" |
| 86 | + try: |
| 87 | + with open(path, "r", encoding="utf-8") as f: |
| 88 | + # Ignore DATA_VERSION, V3 build files had it V4 doesn't |
| 89 | + version_dict = {k.lower(): v for k, v in json.load(f).items() if k != "DATA_VERSION"} |
| 90 | + except (OSError, json.JSONDecodeError) as err: |
| 91 | + raise OSError(f"Couldn't get the build information from {path}: {err}") from err |
| 92 | + |
| 93 | + return SzBuildDetails(**version_dict) |
| 94 | + |
| 95 | + |
| 96 | +def copy_files_dirs(to_copy: dict[str, Any], source_dir: Path, target_dir: Path) -> None: |
| 97 | + """Copy files/directories within and to a project""" |
| 98 | + for c_path, c_dict in to_copy.items(): |
| 99 | + excludes = c_dict["excludes"] |
| 100 | + files = c_dict["files"] |
| 101 | + source = source_dir / c_path |
| 102 | + |
| 103 | + try: |
| 104 | + if source.is_dir(): |
| 105 | + # If the key in to_copy ends with / copy everything in source dir to target_dir |
| 106 | + # er/ as the key copies everything from /opt/senzing/er to target_dir |
| 107 | + # |
| 108 | + # If the key in to_copy doesn't end with / copy source dir and everything in it to target_dir |
| 109 | + # data as the key copies /opt/senzing/data to target_dir/data |
| 110 | + target = target_dir if c_path.endswith("/") else target_dir / c_path |
| 111 | + |
| 112 | + # Copy entire contents of the source directory |
| 113 | + if not files or (files and files[0] == "*"): |
| 114 | + shutil.copytree(source, target, ignore=shutil.ignore_patterns(*excludes), dirs_exist_ok=True) |
| 115 | + |
| 116 | + # Create the source directory in the target and only copy listed files |
| 117 | + if files and files[0] != "*": |
| 118 | + target.mkdir(exist_ok=True, parents=True) |
| 119 | + for source_file in [source / f for f in files]: |
| 120 | + shutil.copy(source_file, target / source_file.name) |
| 121 | + |
| 122 | + if source.is_file(): |
| 123 | + # Single file copy always copies only the file, if the key to to_copy is er/szBuildVersion.json |
| 124 | + # szBuildVersion.json is copied to target_dir and not target_dir/er/szBuildVersion.json |
| 125 | + target = target_dir / source.name |
| 126 | + target_dir.mkdir(exist_ok=True, parents=True) |
| 127 | + shutil.copy(source, target) |
| 128 | + except OSError as err: |
| 129 | + raise OSError(f"Couldn't copy a file or directory: {err}") from err |
| 130 | + |
| 131 | + |
| 132 | +def setup_env(proj_path: Path) -> None: |
| 133 | + """Create a new setupEnv and replace place holders with paths for the project""" |
| 134 | + try: |
| 135 | + shutil.copy(proj_path / "resources/templates/setupEnv", proj_path) |
| 136 | + setup_path = proj_path / "setupEnv" |
| 137 | + |
| 138 | + with open(setup_path, "r", encoding="utf-8") as in_: |
| 139 | + data = in_.read() |
| 140 | + |
| 141 | + data = data.replace("${SENZING_DIR}", str(proj_path)).replace("${SENZING_CONFIG_PATH}", str(proj_path / "etc")) |
| 142 | + |
| 143 | + with open(setup_path, "w", encoding="utf-8") as out: |
| 144 | + out.write(data) |
| 145 | + except OSError as err: |
| 146 | + raise OSError(f"Couldn't create a new setupEnv file: {err}") from err |
| 147 | + |
| 148 | + |
| 149 | +def set_permissions(proj_path: Path, permissions: dict[str, dict[str, Any]]) -> None: |
| 150 | + """Set permissions for files/dirs copied to the project, or dirs removed and replaced completely e.g., data/""" |
| 151 | + try: |
| 152 | + for p_path, p_dict in permissions.items(): |
| 153 | + dir_pint = p_dict["dir_pint"] |
| 154 | + file_pint = p_dict["file_pint"] |
| 155 | + files = p_dict["files"] |
| 156 | + recursive = p_dict["recursive"] |
| 157 | + target = proj_path if p_path.startswith(".") else proj_path / p_path |
| 158 | + excludes = [target / e for e in p_dict["excludes"]] |
| 159 | + |
| 160 | + if target.is_dir(): |
| 161 | + if dir_pint != 0: |
| 162 | + target.chmod(dir_pint) |
| 163 | + d_chmods = ( |
| 164 | + [d for d in target.glob("*") if d.is_dir() and not d.is_symlink() and d not in excludes] |
| 165 | + if not recursive |
| 166 | + else [d for d in target.rglob("*") if d.is_dir() and not d.is_symlink() and d not in excludes] |
| 167 | + ) |
| 168 | + for dir_ in d_chmods: |
| 169 | + dir_.chmod(dir_pint) |
| 170 | + |
| 171 | + if files and files[0] == "*": |
| 172 | + f_chmods = ( |
| 173 | + [f for f in target.glob("*") if f.is_file() and not f.is_symlink() and f not in excludes] |
| 174 | + if not recursive |
| 175 | + else [f for f in target.rglob("*") if f.is_file() and not f.is_symlink() and f not in excludes] |
| 176 | + ) |
| 177 | + |
| 178 | + for file in f_chmods: |
| 179 | + Path(target / file).chmod(file_pint) |
| 180 | + |
| 181 | + if files and files[0] != "*": |
| 182 | + for file in files: |
| 183 | + Path(target / file).chmod(file_pint) |
| 184 | + |
| 185 | + if target.is_file(): |
| 186 | + target.chmod(file_pint) |
| 187 | + except OSError as err: |
| 188 | + raise OSError(f"Couldn't set a permission: {err}") from err |
0 commit comments