import datetime import inspect import os import re import subprocess import sys import tempfile from typing import List from typing import Optional from .connection import Connection from .connection import GiteaHTTPResponse class GiteaModel: def __init__( self, data, *, check_data: bool = True, response: Optional[GiteaHTTPResponse] = None, conn: Optional[Connection] = None, ): if check_data and not isinstance(data, dict): # Gitea sometimes fails to serialize an object and returns ``None`` instead raise ValueError(f"Unable to instantiate model {self.__class__.__name__} from the following data: {data}") self._data = data self._response = response self._conn = conn def dict(self, exclude_columns: Optional[List[str]] = None): exclude_columns = exclude_columns or [] result = {} for mro in inspect.getmro(self.__class__): for name, value in vars(mro).items(): if name.endswith("_obj"): continue found = 0 for i in exclude_columns: if i == name: found = 1 break if found: continue if isinstance(value, property): obj = getattr(self, name) try: result[name] = obj except Exception: pass # ignore objects that cannot fit to dictionary return result def get_editor() -> List[str]: import shutil import shlex from .exceptions import GitObsRuntimeError editor = os.getenv("EDITOR", None) if editor: candidates = [editor] else: candidates = ["vim", "vi"] editor_path = None args = None for i in candidates: i, *args = shlex.split(i) if i.startswith("/"): editor_path = i else: editor_path = shutil.which(i) if editor_path: break if not editor_path: raise GitObsRuntimeError(f"Unable to start editor '{candidates[0]}'") res = [editor_path] if args: res += args return res def get_editor_command(file_path: str) -> List[str]: res = get_editor() res.append(file_path) return res def run_editor(file_path: str): subprocess.run(get_editor_command(file_path), check=True) def edit_message(template: Optional[str] = None) -> str: with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8", prefix="git_obs_message_") as f: if template: f.write(template) f.flush() run_editor(f.name) f.seek(0) return f.read() def dt_sanitize(date_time: str): """ Sanitize ``date_time`` string to "YYYY-MM-DD HH:MM" UTC. The time zone offset must be in the '[+-]HH:MM' format or 'Z' which stands for UTC """ if sys.version_info[:2] <= (3, 10): # python 3.10 doesn't support "Z" offset in fromisoformat() # this also fixes the offset for strptime() in python 3.6 if date_time.endswith("Z"): date_time = f"{date_time[:-1]}+00:00" if sys.version_info[:2] <= (3, 6): # python 3.6 doesn't support fromisoformat(), we need to use strptime() instead # unfortunately strptime() is outdated and needs removing colons from the time offset match = re.match(r"^([\d-]+[ T][\d:]+)(Z|[\+\-][\d:]+)", date_time) if match: dt = match.group(1) offset = match.group(2) date_time = f"{dt}{offset.replace(':', '')}" dt = datetime.datetime.strptime(date_time, "%Y-%m-%dT%H:%M:%S%z") else: dt = datetime.datetime.fromisoformat(date_time) return dt.astimezone(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M") if sys.version_info[:2] >= (3, 12): TemporaryDirectory = tempfile.TemporaryDirectory else: class TemporaryDirectory: """ A minimalist implementation of TemporaryDirectory that supports 'delete' argument """ def __init__(self, suffix=None, prefix=None, dir=None, delete=True): self.name = tempfile.mkdtemp(suffix=suffix, prefix=prefix, dir=dir) self._delete = delete def __enter__(self): return self.name def __exit__(self, exc_type, exc_val, exc_tb): import shutil if self._delete and os.path.isdir(self.name): shutil.rmtree(self.name)