diff --git a/.github/workflows/run_program.yml b/.github/workflows/run_program.yml new file mode 100644 index 0000000..43799b5 --- /dev/null +++ b/.github/workflows/run_program.yml @@ -0,0 +1,46 @@ +name: Run Program + +on: + workflow_dispatch: + push: + paths: + - src/program/** + - pyproject.toml + pull_request: + paths: + - src/program/** + - pyproject.toml + +jobs: + run: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: 3.13 + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install uv + uv sync + + - name: Run Program + run: | + uv pip install -e . + uv run python src/program/main.py + + - name: Upload Logs + if: always() + uses: actions/upload-artifact@v7 + with: + name: logs + path: logs/ + archive: true + retention-days: 90 + if-no-files-found: ignore diff --git a/pyproject.toml b/pyproject.toml index 12ecbb1..e11810c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,11 @@ -[build-system] -requires = ["uv_build >= 0.9.2, <0.10.0"] -build-backend = "uv_build" - [project] -name = "math-modeling-template" +name = "program" version = "0.1.0" description = "Add your descriptions here" dependencies = [ "numpy", "pandas", + "colorlog", ] authors = [ {name = "Example Person", email = "someone@example.com"}, diff --git a/src/.gitkeep b/src/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/src/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/program/main.py b/src/program/main.py new file mode 100644 index 0000000..83f52e5 --- /dev/null +++ b/src/program/main.py @@ -0,0 +1,7 @@ +from program.utils.logger import logger + +def main(): + logger.info("Hello World!") + +if __name__ == "__main__": + main() diff --git a/src/program/utils/logger.py b/src/program/utils/logger.py new file mode 100644 index 0000000..205a7eb --- /dev/null +++ b/src/program/utils/logger.py @@ -0,0 +1,142 @@ +"""Logger, 主要由 DeepSeek 生成""" + +import datetime +import glob +import logging +import os +import zipfile + +import colorlog + + +class _FileFormatter(logging.Formatter): + """自定义文件格式化器,在时间末尾追加毫秒""" + + def formatTime(self, record, datefmt=None): + ct = self.converter(record.created) + if datefmt: + base_time = datetime.datetime(*ct[:6]).strftime(datefmt) + else: + base_time = datetime.datetime(*ct[:6]).strftime("%Y-%m-%d %H:%M:%S") + return f"{base_time}.{int(record.msecs):03d}" + + +def _archive_old_log(log_dir: str, log_filename: str): + """ + 将上一次运行留下的 program.log 压缩为 program-{date}-{times}.zip。 + date 取自日志文件的修改时间,times 为当天序号(自动递增)。 + """ + log_path = os.path.join(log_dir, log_filename) + if not os.path.isfile(log_path): + return + + # 以文件修改时间作为日志产生的日期 + mtime = os.path.getmtime(log_path) + date_str = datetime.datetime.fromtimestamp(mtime).strftime("%Y-%m-%d") + + # 查找同一天已有的压缩包,确定序号 + pattern = os.path.join(log_dir, f"program-{date_str}-*.zip") + existing = glob.glob(pattern) + max_idx = 0 + for path in existing: + name = os.path.splitext(os.path.basename(path))[0] # program-{date}-{idx} + parts = name.split("-") + if len(parts) >= 4 and parts[-1].isdigit(): + idx = int(parts[-1]) + if idx > max_idx: + max_idx = idx + times = max_idx + 1 + + zip_filename = f"program-{date_str}-{times}.zip" + zip_path = os.path.join(log_dir, zip_filename) + + try: + with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(log_path, arcname=log_filename) + os.remove(log_path) # 压缩成功后删除原文件 + except Exception as e: + print(f"归档旧日志失败: {e}") + + +_configured = False + + +def setup_logging(log_dir: str = "logs", + log_filename: str = "program.log", + console_level: int = logging.INFO, + file_level: int = logging.DEBUG, + ): + """ + 配置全局日志系统(仅首次调用有效)。 + 控制台输出格式:[HH:MM] [ThreadName/LEVEL] [filename]: message + 文件输出格式: [YYYY-MM-DD HH:MM:SS.ms] [ThreadName/LEVEL] [filename(funcName)]: message + """ + global _configured + if _configured: + return + + os.makedirs(log_dir, exist_ok=True) + _archive_old_log(log_dir, log_filename) + + # 根 Logger 设为最低级别,由 Handler 各自控制 + root = logging.getLogger() + root.setLevel(logging.DEBUG) + root.handlers.clear() + + # --- 控制台 Handler(带颜色)--- + console_fmt = colorlog.ColoredFormatter( + "[%(asctime)s] [%(threadName)s/%(log_color)s%(levelname)s%(reset)s] [%(filename)s]: %(message)s", + datefmt="%H:%M", + log_colors={ + "DEBUG": "cyan", + "INFO": "green", + "WARNING": "yellow", + "ERROR": "red", + "CRITICAL": "red,bg_white", + }, + ) + console_handler = logging.StreamHandler() + console_handler.setLevel(console_level) + console_handler.setFormatter(console_fmt) + root.addHandler(console_handler) + + # --- 文件 Handler(带毫秒)--- + file_fmt = _FileFormatter( + "[%(asctime)s] [%(threadName)s/%(levelname)s] [%(filename)s(%(funcName)s)]: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + file_handler = logging.FileHandler( + os.path.join(log_dir, log_filename), mode="w", encoding="utf-8", + ) + file_handler.setLevel(file_level) + file_handler.setFormatter(file_fmt) + root.addHandler(file_handler) + + _configured = True + + +def get_logger() -> logging.Logger: + """ + 获取已配置好格式的 Logger。 + 首次调用会自动初始化日志系统(setup_logging)。 + """ + setup_logging() + return logging.getLogger() + + +logger = get_logger() +debug = logger.debug +info = logger.info +warning = logger.warning +error = logger.error +critical = logger.critical +exception = logger.exception + +# # ============ 使用示例 ============ +# if __name__ == "__main__": +# logger = get_logger(__name__) +# logger.debug("这是一条 DEBUG 日志(仅文件)") +# logger.info("HelloWorld") +# logger.warning("警告信息") +# logger.error("错误信息") +# logger.critical("严重错误")