apb_extra_utils.github
1import functools 2import json 3import os 4import shutil 5from tempfile import mkdtemp 6from urllib.error import HTTPError 7from urllib.request import Request, urlopen 8 9from apb_extra_utils.misc import download_and_unzip, remove_content_dir, zip_dir, create_dir 10 11PREFIX_FILE_LAST_TAG_REPO = 'last_tag_repo_github_' 12from pathlib import Path 13 14TEXT_EXTS = { 15 ".py", ".txt", ".md", ".rst", ".json", ".yaml", ".yml", 16 ".ini", ".cfg", ".toml", ".csv", ".tsv", ".xml", ".html", 17 ".css", ".js", ".sql", ".bat", ".cmd", ".ps1" 18} 19 20def _is_probably_binary(data: bytes) -> bool: 21 return b"\x00" in data 22 23def convert_tree_to_crlf(root_dir: str): 24 """ 25 Convert endlines LF to CRLF 26 27 Args: 28 root_dir (str): root directory of tree: 29 30 Returns: 31 None 32 """ 33 root = Path(root_dir) 34 for p in root.rglob("*"): 35 if not p.is_file(): 36 continue 37 if p.suffix.lower() not in TEXT_EXTS: 38 continue 39 40 raw = p.read_bytes() 41 if _is_probably_binary(raw): 42 continue 43 44 # decode defensivo 45 try: 46 text = raw.decode("utf-8") 47 encoding = "utf-8" 48 except UnicodeDecodeError: 49 try: 50 text = raw.decode("windows-1252") 51 encoding = "windows-1252" 52 except UnicodeDecodeError: 53 continue 54 55 # normaliza primero a LF y luego a CRLF 56 text = text.replace("\r\n", "\n").replace("\r", "\n") 57 text = text.replace("\n", "\r\n") 58 59 p.write_text(text, encoding=encoding, newline="") 60 61 62def get_api_github(owner, repo, api_request, token=None): 63 """ 64 GET Request for repository GITHUB via API GITHUB. 65 66 See the REST API DOCS here https://docs.github.com/en/rest 67 68 Args: 69 owner (str): 70 repo (str): 71 api_request (str): 72 token (str=None): 73 74 Returns: 75 info_response (dict) 76 """ 77 request_headers = { 78 'Accept': 'application/vnd.github.v3+json' 79 } 80 if token: 81 request_headers['Authorization'] = f'token {token}' 82 83 url_github = f'https://api.github.com/repos/{owner}/{repo}/{api_request}' 84 req = Request(url_github, headers=request_headers, method='GET') 85 info_response = {} 86 try: 87 with urlopen(req) as resp_request: 88 if resp_request: 89 info_response = json.load(resp_request) 90 except HTTPError as exc: 91 info_response[HTTPError.__name__] = str(exc) 92 93 return info_response 94 95 96def post_api_github(owner, repo, api_request, post_data, token=None): 97 """ 98 POST Request on repository GITHUB via API GITHUB 99 100 See the REST API DOCS here https://docs.github.com/en/rest 101 102 Args: 103 owner (str): 104 repo (str): 105 api_request (str): 106 post_data (dict): 107 token (str=None): 108 109 Returns: 110 info_response (dict) 111 """ 112 request_headers = { 113 'Accept': 'application/vnd.github.v3+json', 114 'Content-Type': 'application/json; charset=utf-8', 115 } 116 if token: 117 request_headers['Authorization'] = f'token {token}' 118 119 url_github = f'https://api.github.com/repos/{owner}/{repo}/{api_request}' 120 121 post_data_enc = json.dumps(post_data).encode('utf-8') 122 123 req = Request(url_github, headers=request_headers, method='POST') 124 info_response = {} 125 try: 126 with urlopen(req, post_data_enc) as resp_request: 127 if resp_request: 128 info_response = resp_request.__dict__ 129 except HTTPError as exc: 130 info_response[HTTPError.__name__] = str(exc) 131 132 return info_response 133 134 135@functools.cache 136def has_changes_in_github(owner, repo, branch, download_to, token=None): 137 """ 138 Check if the GitHub repository branch has changes. 139 140 Args: 141 owner (str): Owner repository Github 142 repo (str): Name repository Github 143 branch (str): Branch repository to check 144 download_to (str): Path to the local repository 145 token (str=None): Github token for private access 146 147 Returns: 148 bool: True if there are changes, False otherwise 149 str: sha_commit of the current branch state 150 """ 151 branch = branch.lower() 152 info_branches = get_api_github(owner, repo, 'branches', token) 153 info_branch = next(filter(lambda el: el.get('name', '').lower() == branch, 154 info_branches), None) 155 156 if not info_branch: 157 return False, None 158 159 sha_commit = info_branch.get('commit').get('sha') 160 expected_name_zip_repo = f'{repo}-{branch}' 161 log_last_tag = os.path.join(download_to, f'.{PREFIX_FILE_LAST_TAG_REPO}{expected_name_zip_repo}') 162 163 if os.path.exists(log_last_tag): 164 with open(log_last_tag) as fr: 165 last_tag = fr.read() 166 if last_tag and last_tag.strip() == sha_commit.strip(): 167 return False, sha_commit 168 169 return True, sha_commit 170 171 172def get_resources_from_repo_github(html_repo, tag, expected_name_zip_repo, path_repo, header=None, force_update=False, 173 remove_prev=False, as_zip=False, normalize_eol_win32=None): 174 """ 175 176 Args: 177 html_repo (str): 178 tag (str): 179 expected_name_zip_repo (str): 180 path_repo (str): 181 header (dict=None): 182 force_update (bool=False): 183 remove_prev (bool=False): 184 as_zip (bool=False): 185 normalize_eol_win32 (bool=None): Normaliza endline para CRLF. Si True fuerza la conversión; 186 si None convierte solo en Windows; si False no convierte. 187 188 Returns: 189 updated (bool) 190 """ 191 updated = False 192 log_last_tag = os.path.join(path_repo, f'.{PREFIX_FILE_LAST_TAG_REPO}{expected_name_zip_repo}') 193 194 if not force_update: 195 last_tag = None 196 if os.path.exists(log_last_tag): 197 with open(log_last_tag) as fr: 198 last_tag = fr.read() 199 200 if last_tag and last_tag.strip() == tag.strip(): 201 return updated 202 203 if not header: 204 header = {} 205 header['Accept'] = 'application/octet-stream' 206 207 dir_temp = mkdtemp() 208 download_and_unzip(html_repo, extract_to=dir_temp, headers=[(k, v) for k, v in header.items()]) 209 path_res = os.path.join(dir_temp, expected_name_zip_repo) 210 211 if os.path.exists(path_res): 212 create_dir(path_repo) 213 214 if as_zip: 215 zip_dir(path_res, os.path.join(path_repo, f'{expected_name_zip_repo}.zip')) 216 else: 217 if remove_prev and os.path.exists(path_repo): 218 remove_content_dir(path_repo) 219 shutil.copytree(path_res, path_repo, dirs_exist_ok=True) 220 if normalize_eol_win32 is True or (normalize_eol_win32 is None and os.name == 'nt'): 221 convert_tree_to_crlf(path_repo) 222 223 shutil.rmtree(path_res, ignore_errors=True) 224 225 with open(log_last_tag, "w+") as fw: 226 fw.write(tag) 227 228 updated = True 229 230 return updated 231 232 233def download_release_repo_github(owner, repo, download_to, tag_release=None, token=None, force=False, as_zip=False, 234 remove_prev=False, normalize_eol_win32=None): 235 """ 236 Download release Github repository on the path selected. 237 238 Args: 239 owner (str): Owner repository Github 240 repo (str): Name repository Github 241 download_to (str): Path to download 242 tag_release (str=None): if not informed get 'latest' release 243 token (str=None): Github token for private access 244 force (bool=False): Force update if exists previous sources 245 remove_prev (bool=False): Remove all previous resources 246 as_zip (bool=False): Retorna como ZIP 247 normalize_eol_win32 (bool=None): Normaliza endline para CRLF. Si True fuerza la conversión; 248 si None convierte solo en Windows; si False no convierte. 249 250 Returns: 251 tag_name (str) 252 """ 253 if not tag_release: 254 info_release = get_api_github(owner, repo, 'releases/latest', token) 255 else: 256 info_release = get_api_github(owner, repo, f'releases/tags/{tag_release}', token) 257 258 tag_name = info_release.get('tag_name') 259 if tag_name: 260 html_release = f'https://github.com/{owner}/{repo}/archive/refs/tags/{tag_name}.zip' 261 header = {} 262 if token: 263 header['Authorization'] = f'token {token}' 264 265 get_resources_from_repo_github(html_release, tag_name, f'{repo}-{tag_name}', download_to, header=header, 266 force_update=force, remove_prev=remove_prev, as_zip=as_zip, normalize_eol_win32=normalize_eol_win32) 267 268 return tag_name 269 270 271def download_branch_repo_github(owner, repo, branch, download_to, token=None, force=False, as_zip=False, 272 remove_prev=False, normalize_eol_win32=None): 273 """ 274 Download the branch selected for the Github repo on the path selected 275 276 Args: 277 owner (str): Owner repository Github 278 repo (str): Name repository Github 279 branch (str): Branch repository to download 280 download_to (str): Path to download 281 token (str=None): Github token for private access 282 force (bool=False): Force update if exists previous sources 283 remove_prev (bool=False): Remove all previous resources 284 as_zip (bool=False): Retorna como ZIP 285 normalize_eol_win32 (bool=None): Normaliza endline para CRLF. Si True fuerza la conversión; 286 si None convierte solo en Windows; si False no convierte. 287 288 Returns: 289 sha_commit (str), updated (boolean) 290 """ 291 has_changes, sha_commit = has_changes_in_github(owner, repo, branch, download_to, token) 292 if not has_changes and not force: 293 return sha_commit, False 294 html_branch = f'https://github.com/{owner}/{repo}/archive/refs/heads/{branch}.zip' 295 header = {} 296 if token: 297 header['Authorization'] = f'token {token}' 298 299 name_zip = f'{repo}-{branch}' 300 updated = get_resources_from_repo_github(html_branch, sha_commit, name_zip, download_to, header=header, 301 force_update=force, remove_prev=remove_prev, as_zip=as_zip, normalize_eol_win32=normalize_eol_win32) 302 303 if as_zip: 304 path_zip = os.path.join(download_to, f'{name_zip}.zip') 305 if os.path.exists(path_zip): 306 new_path_zip = os.path.join(download_to, f'{name_zip}-{sha_commit}.zip') 307 if os.path.exists(new_path_zip): 308 os.remove(new_path_zip) 309 os.rename(path_zip, new_path_zip) 310 311 return sha_commit, updated 312 313 314if __name__ == '__main__': 315 import fire, sys 316 317 sys.exit(fire.Fire( 318 { 319 get_api_github.__name__: get_api_github, 320 post_api_github.__name__: post_api_github, 321 download_release_repo_github.__name__: download_release_repo_github, 322 download_branch_repo_github.__name__: download_branch_repo_github 323 } 324 ))
PREFIX_FILE_LAST_TAG_REPO =
'last_tag_repo_github_'
TEXT_EXTS =
{'.cfg', '.py', '.toml', '.bat', '.yaml', '.txt', '.json', '.tsv', '.rst', '.yml', '.ini', '.xml', '.sql', '.md', '.ps1', '.cmd', '.js', '.html', '.css', '.csv'}
def
convert_tree_to_crlf(root_dir: str):
24def convert_tree_to_crlf(root_dir: str): 25 """ 26 Convert endlines LF to CRLF 27 28 Args: 29 root_dir (str): root directory of tree: 30 31 Returns: 32 None 33 """ 34 root = Path(root_dir) 35 for p in root.rglob("*"): 36 if not p.is_file(): 37 continue 38 if p.suffix.lower() not in TEXT_EXTS: 39 continue 40 41 raw = p.read_bytes() 42 if _is_probably_binary(raw): 43 continue 44 45 # decode defensivo 46 try: 47 text = raw.decode("utf-8") 48 encoding = "utf-8" 49 except UnicodeDecodeError: 50 try: 51 text = raw.decode("windows-1252") 52 encoding = "windows-1252" 53 except UnicodeDecodeError: 54 continue 55 56 # normaliza primero a LF y luego a CRLF 57 text = text.replace("\r\n", "\n").replace("\r", "\n") 58 text = text.replace("\n", "\r\n") 59 60 p.write_text(text, encoding=encoding, newline="")
Convert endlines LF to CRLF
Arguments:
- root_dir (str): root directory of tree:
Returns:
None
def
get_api_github(owner, repo, api_request, token=None):
63def get_api_github(owner, repo, api_request, token=None): 64 """ 65 GET Request for repository GITHUB via API GITHUB. 66 67 See the REST API DOCS here https://docs.github.com/en/rest 68 69 Args: 70 owner (str): 71 repo (str): 72 api_request (str): 73 token (str=None): 74 75 Returns: 76 info_response (dict) 77 """ 78 request_headers = { 79 'Accept': 'application/vnd.github.v3+json' 80 } 81 if token: 82 request_headers['Authorization'] = f'token {token}' 83 84 url_github = f'https://api.github.com/repos/{owner}/{repo}/{api_request}' 85 req = Request(url_github, headers=request_headers, method='GET') 86 info_response = {} 87 try: 88 with urlopen(req) as resp_request: 89 if resp_request: 90 info_response = json.load(resp_request) 91 except HTTPError as exc: 92 info_response[HTTPError.__name__] = str(exc) 93 94 return info_response
GET Request for repository GITHUB via API GITHUB.
See the REST API DOCS here apb_extra_utils.github.com/en/rest">https://docsapb_extra_utils.github.com/en/rest
Arguments:
- owner (str):
- repo (str):
- api_request (str):
- token (str=None):
Returns:
info_response (dict)
def
post_api_github(owner, repo, api_request, post_data, token=None):
97def post_api_github(owner, repo, api_request, post_data, token=None): 98 """ 99 POST Request on repository GITHUB via API GITHUB 100 101 See the REST API DOCS here https://docs.github.com/en/rest 102 103 Args: 104 owner (str): 105 repo (str): 106 api_request (str): 107 post_data (dict): 108 token (str=None): 109 110 Returns: 111 info_response (dict) 112 """ 113 request_headers = { 114 'Accept': 'application/vnd.github.v3+json', 115 'Content-Type': 'application/json; charset=utf-8', 116 } 117 if token: 118 request_headers['Authorization'] = f'token {token}' 119 120 url_github = f'https://api.github.com/repos/{owner}/{repo}/{api_request}' 121 122 post_data_enc = json.dumps(post_data).encode('utf-8') 123 124 req = Request(url_github, headers=request_headers, method='POST') 125 info_response = {} 126 try: 127 with urlopen(req, post_data_enc) as resp_request: 128 if resp_request: 129 info_response = resp_request.__dict__ 130 except HTTPError as exc: 131 info_response[HTTPError.__name__] = str(exc) 132 133 return info_response
POST Request on repository GITHUB via API GITHUB
See the REST API DOCS here apb_extra_utils.github.com/en/rest">https://docsapb_extra_utils.github.com/en/rest
Arguments:
- owner (str):
- repo (str):
- api_request (str):
- post_data (dict):
- token (str=None):
Returns:
info_response (dict)
@functools.cache
def
has_changes_in_github(owner, repo, branch, download_to, token=None):
136@functools.cache 137def has_changes_in_github(owner, repo, branch, download_to, token=None): 138 """ 139 Check if the GitHub repository branch has changes. 140 141 Args: 142 owner (str): Owner repository Github 143 repo (str): Name repository Github 144 branch (str): Branch repository to check 145 download_to (str): Path to the local repository 146 token (str=None): Github token for private access 147 148 Returns: 149 bool: True if there are changes, False otherwise 150 str: sha_commit of the current branch state 151 """ 152 branch = branch.lower() 153 info_branches = get_api_github(owner, repo, 'branches', token) 154 info_branch = next(filter(lambda el: el.get('name', '').lower() == branch, 155 info_branches), None) 156 157 if not info_branch: 158 return False, None 159 160 sha_commit = info_branch.get('commit').get('sha') 161 expected_name_zip_repo = f'{repo}-{branch}' 162 log_last_tag = os.path.join(download_to, f'.{PREFIX_FILE_LAST_TAG_REPO}{expected_name_zip_repo}') 163 164 if os.path.exists(log_last_tag): 165 with open(log_last_tag) as fr: 166 last_tag = fr.read() 167 if last_tag and last_tag.strip() == sha_commit.strip(): 168 return False, sha_commit 169 170 return True, sha_commit
Check if the GitHub repository branch has changes.
Arguments:
- owner (str): Owner repository Github
- repo (str): Name repository Github
- branch (str): Branch repository to check
- download_to (str): Path to the local repository
- token (str=None): Github token for private access
Returns:
bool: True if there are changes, False otherwise str: sha_commit of the current branch state
def
get_resources_from_repo_github( html_repo, tag, expected_name_zip_repo, path_repo, header=None, force_update=False, remove_prev=False, as_zip=False, normalize_eol_win32=None):
173def get_resources_from_repo_github(html_repo, tag, expected_name_zip_repo, path_repo, header=None, force_update=False, 174 remove_prev=False, as_zip=False, normalize_eol_win32=None): 175 """ 176 177 Args: 178 html_repo (str): 179 tag (str): 180 expected_name_zip_repo (str): 181 path_repo (str): 182 header (dict=None): 183 force_update (bool=False): 184 remove_prev (bool=False): 185 as_zip (bool=False): 186 normalize_eol_win32 (bool=None): Normaliza endline para CRLF. Si True fuerza la conversión; 187 si None convierte solo en Windows; si False no convierte. 188 189 Returns: 190 updated (bool) 191 """ 192 updated = False 193 log_last_tag = os.path.join(path_repo, f'.{PREFIX_FILE_LAST_TAG_REPO}{expected_name_zip_repo}') 194 195 if not force_update: 196 last_tag = None 197 if os.path.exists(log_last_tag): 198 with open(log_last_tag) as fr: 199 last_tag = fr.read() 200 201 if last_tag and last_tag.strip() == tag.strip(): 202 return updated 203 204 if not header: 205 header = {} 206 header['Accept'] = 'application/octet-stream' 207 208 dir_temp = mkdtemp() 209 download_and_unzip(html_repo, extract_to=dir_temp, headers=[(k, v) for k, v in header.items()]) 210 path_res = os.path.join(dir_temp, expected_name_zip_repo) 211 212 if os.path.exists(path_res): 213 create_dir(path_repo) 214 215 if as_zip: 216 zip_dir(path_res, os.path.join(path_repo, f'{expected_name_zip_repo}.zip')) 217 else: 218 if remove_prev and os.path.exists(path_repo): 219 remove_content_dir(path_repo) 220 shutil.copytree(path_res, path_repo, dirs_exist_ok=True) 221 if normalize_eol_win32 is True or (normalize_eol_win32 is None and os.name == 'nt'): 222 convert_tree_to_crlf(path_repo) 223 224 shutil.rmtree(path_res, ignore_errors=True) 225 226 with open(log_last_tag, "w+") as fw: 227 fw.write(tag) 228 229 updated = True 230 231 return updated
Arguments:
- html_repo (str):
- tag (str):
- expected_name_zip_repo (str):
- path_repo (str):
- header (dict=None):
- force_update (bool=False):
- remove_prev (bool=False):
- as_zip (bool=False):
- normalize_eol_win32 (bool=None): Normaliza endline para CRLF. Si True fuerza la conversión; si None convierte solo en Windows; si False no convierte.
Returns:
updated (bool)
def
download_release_repo_github( owner, repo, download_to, tag_release=None, token=None, force=False, as_zip=False, remove_prev=False, normalize_eol_win32=None):
234def download_release_repo_github(owner, repo, download_to, tag_release=None, token=None, force=False, as_zip=False, 235 remove_prev=False, normalize_eol_win32=None): 236 """ 237 Download release Github repository on the path selected. 238 239 Args: 240 owner (str): Owner repository Github 241 repo (str): Name repository Github 242 download_to (str): Path to download 243 tag_release (str=None): if not informed get 'latest' release 244 token (str=None): Github token for private access 245 force (bool=False): Force update if exists previous sources 246 remove_prev (bool=False): Remove all previous resources 247 as_zip (bool=False): Retorna como ZIP 248 normalize_eol_win32 (bool=None): Normaliza endline para CRLF. Si True fuerza la conversión; 249 si None convierte solo en Windows; si False no convierte. 250 251 Returns: 252 tag_name (str) 253 """ 254 if not tag_release: 255 info_release = get_api_github(owner, repo, 'releases/latest', token) 256 else: 257 info_release = get_api_github(owner, repo, f'releases/tags/{tag_release}', token) 258 259 tag_name = info_release.get('tag_name') 260 if tag_name: 261 html_release = f'https://github.com/{owner}/{repo}/archive/refs/tags/{tag_name}.zip' 262 header = {} 263 if token: 264 header['Authorization'] = f'token {token}' 265 266 get_resources_from_repo_github(html_release, tag_name, f'{repo}-{tag_name}', download_to, header=header, 267 force_update=force, remove_prev=remove_prev, as_zip=as_zip, normalize_eol_win32=normalize_eol_win32) 268 269 return tag_name
Download release Github repository on the path selected.
Arguments:
- owner (str): Owner repository Github
- repo (str): Name repository Github
- download_to (str): Path to download
- tag_release (str=None): if not informed get 'latest' release
- token (str=None): Github token for private access
- force (bool=False): Force update if exists previous sources
- remove_prev (bool=False): Remove all previous resources
- as_zip (bool=False): Retorna como ZIP
- normalize_eol_win32 (bool=None): Normaliza endline para CRLF. Si True fuerza la conversión; si None convierte solo en Windows; si False no convierte.
Returns:
tag_name (str)
def
download_branch_repo_github( owner, repo, branch, download_to, token=None, force=False, as_zip=False, remove_prev=False, normalize_eol_win32=None):
272def download_branch_repo_github(owner, repo, branch, download_to, token=None, force=False, as_zip=False, 273 remove_prev=False, normalize_eol_win32=None): 274 """ 275 Download the branch selected for the Github repo on the path selected 276 277 Args: 278 owner (str): Owner repository Github 279 repo (str): Name repository Github 280 branch (str): Branch repository to download 281 download_to (str): Path to download 282 token (str=None): Github token for private access 283 force (bool=False): Force update if exists previous sources 284 remove_prev (bool=False): Remove all previous resources 285 as_zip (bool=False): Retorna como ZIP 286 normalize_eol_win32 (bool=None): Normaliza endline para CRLF. Si True fuerza la conversión; 287 si None convierte solo en Windows; si False no convierte. 288 289 Returns: 290 sha_commit (str), updated (boolean) 291 """ 292 has_changes, sha_commit = has_changes_in_github(owner, repo, branch, download_to, token) 293 if not has_changes and not force: 294 return sha_commit, False 295 html_branch = f'https://github.com/{owner}/{repo}/archive/refs/heads/{branch}.zip' 296 header = {} 297 if token: 298 header['Authorization'] = f'token {token}' 299 300 name_zip = f'{repo}-{branch}' 301 updated = get_resources_from_repo_github(html_branch, sha_commit, name_zip, download_to, header=header, 302 force_update=force, remove_prev=remove_prev, as_zip=as_zip, normalize_eol_win32=normalize_eol_win32) 303 304 if as_zip: 305 path_zip = os.path.join(download_to, f'{name_zip}.zip') 306 if os.path.exists(path_zip): 307 new_path_zip = os.path.join(download_to, f'{name_zip}-{sha_commit}.zip') 308 if os.path.exists(new_path_zip): 309 os.remove(new_path_zip) 310 os.rename(path_zip, new_path_zip) 311 312 return sha_commit, updated
Download the branch selected for the Github repo on the path selected
Arguments:
- owner (str): Owner repository Github
- repo (str): Name repository Github
- branch (str): Branch repository to download
- download_to (str): Path to download
- token (str=None): Github token for private access
- force (bool=False): Force update if exists previous sources
- remove_prev (bool=False): Remove all previous resources
- as_zip (bool=False): Retorna como ZIP
- normalize_eol_win32 (bool=None): Normaliza endline para CRLF. Si True fuerza la conversión; si None convierte solo en Windows; si False no convierte.
Returns:
sha_commit (str), updated (boolean)