diff --git a/.github/workflows/branch_ci.yml b/.github/workflows/branch_ci.yml new file mode 100644 index 0000000..641c8a8 --- /dev/null +++ b/.github/workflows/branch_ci.yml @@ -0,0 +1,17 @@ +name: Non-Default Branch Push CI (Python) + +on: + push: + branches-ignore: [ "main" ] + paths-ignore: ['README.md'] + +jobs: + branch-ci: + uses: openclimatefix/.github/.github/workflows/nondefault_branch_push_ci_python.yml@main + secrets: inherit + with: + containerfile: Containerfile + enable_linting: true + enable_typechecking: true + tests_folder: tests + diff --git a/.github/workflows/merged_ci.yml b/.github/workflows/merged_ci.yml new file mode 100644 index 0000000..38da9fe --- /dev/null +++ b/.github/workflows/merged_ci.yml @@ -0,0 +1,11 @@ +name: Default Branch PR Merged CI + +on: + pull_request: + types: ["closed"] + branches: [ "main" ] + +jobs: + merged-ci: + uses: openclimatefix/.github/.github/workflows/default_branch_pr_merged_ci.yml@main + secrets: inherit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 9edb1f6..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,20 +0,0 @@ -default_language_version: - python: python3 - -repos: - - repo: "https://github.com/pre-commit/pre-commit-hooks" - rev: v4.4.0 - hooks: - # Supported hooks: https://pre-commit.com/hooks.html - - id: end-of-file-fixer - - id: detect-private-key - - - repo: "https://github.com/astral-sh/ruff-pre-commit" - rev: v0.6.4 - hooks: - # Run the linter and fix problems. - # * Must be run before ruff-format! - - id: ruff - args: [ --fix ] - # Run the formatter. - - id: ruff-format diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..69ba220 --- /dev/null +++ b/Containerfile @@ -0,0 +1,14 @@ +FROM python:3.12-slim-bookworm +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +# Add repository code +WORKDIR /opt/app +COPY src /opt/app/src +COPY pyproject.toml /opt/app +COPY .git /opt/app/.git + +RUN uv sync --no-dev + +ENTRYPOINT ["uv", "run", "--no-dev"] +CMD ["ocf-template-cli"] + diff --git a/pyproject.toml b/pyproject.toml index 63f5a90..cb73890 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ classifiers = [ "License :: OSI Approved :: MIT License", ] dependencies = [ + "loguru >= 0.7.3", "numpy >= 1.23.2", ] @@ -39,6 +40,7 @@ dev = [ [project.scripts] # Put entrypoints in here +ocf-template-cli = "ocf_template.main:main" [project.urls] repository = "https://github.com/openclimatefix/ocf-python-template" diff --git a/src/ocf_template/__init__.py b/src/ocf_template/__init__.py index f788f32..d6c2057 100644 --- a/src/ocf_template/__init__.py +++ b/src/ocf_template/__init__.py @@ -1 +1,53 @@ -"""Source File""" +"""Setup logging configuration for the application.""" + +import json +import sys +import os +import loguru + +def development_formatter(record: "loguru.Record") -> str: + """Format a log record for development.""" + return "".join(( + "{time:HH:mm:ss.SSS} ", + "{level:<7s} [{file}:{line}] | {message} ", + "{extra}" if record["extra"] else "", + "\n{exception}", + )) + +def structured_formatter(record: "loguru.Record") -> str: + """Format a log record as a structured JSON object.""" + record["extra"]["serialized"] = json.dumps({ + "timestamp": record["time"].strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "severity": record["level"].name, + "message": record["message"], + "elapsed": record["elapsed"].total_seconds(), + "logging.googleapis.com/labels": { + "python_logger": record["name"], + }, + "logging.googleapis.com/sourceLocation": { + "file": record["file"].name, + "line": record["line"], + "function": record["function"], + }, + } | record["extra"]) + return "{extra[serialized]}\n" + +# Define the logging formatter, removing the default one +loguru.logger.remove(0) +if sys.stdout.isatty(): + # Simple logging for development + loguru.logger.add( + sys.stdout, format=development_formatter, diagnose=True, + level=os.getenv("LOGLEVEL", "DEBUG"), backtrace=True, colorize=True, + ) +else: + # JSON logging for containers + loguru.logger.add( + sys.stdout, format=structured_formatter, + level=os.getenv("LOGLEVEL", "INFO").upper(), + ) + +# Uncomment and change the list to quieten external libraries +# for logger in ["aiobotocore", "cfgrib"]: +# logging.getLogger(logger).setLevel(logging.WARNING) + diff --git a/src/ocf_template/main.py b/src/ocf_template/main.py new file mode 100644 index 0000000..6c79cd6 --- /dev/null +++ b/src/ocf_template/main.py @@ -0,0 +1,47 @@ +"""Entrypoint to the application.""" + +from importlib.metadata import PackageNotFoundError, version + +from loguru import logger as log + +try: + __version__ = version(__package__) +except PackageNotFoundError: + __version__ = "v?" + +def main() -> None: + """Entrypoint to the application. + + Thanks to the `script` section in `pyproject.toml`, this function is + automatically executed when the package is run as a script via + `ocf-template-cli`. Change name in pyproject to something more suitable + for your package! + """ + log.info("Hello, world!") + log.info("Adding extra context to this log to help with debugging: ", version=__version__) + + log.info("You can also contextualize logs in bulk to save having to provide it repeatedly!") + filenames: list[str] = [f"file_{i}.txt" for i in range(5)] + log.debug("Might be some spooky numbers in these files!", num_files=len(filenames)) + for filename in filenames: + with log.contextualize(filename=filename): + log.debug("Trying to find spooky numbers in file {filename}") + if "4" in filename: + log.warning("Spooky number found!") + else: + log.debug("No spooky numbers found") + + log.info("Different log levels are also nicely formatted") + for i, level in enumerate(["DEBUG", "INFO", "WARNING", "ERROR"]): + log.log(level, f"This is a {level} log message", index=i) + + log.info("Consider wrapping your main in a try/except block to handle exceptions gracefully") + try: + y: int = int(3 / 0) + log.warning("This will never be logged", y=y) + except ZeroDivisionError as e: + log.opt(exception=e).error("Caught exception") + + log.info("And if you run this in a container, you'll see structured logs instead!") + log.info("Goodbye, world!") + diff --git a/tests/test_something.py b/tests/test_something.py new file mode 100644 index 0000000..1d282b8 --- /dev/null +++ b/tests/test_something.py @@ -0,0 +1,7 @@ +import unittest + + +class TestSomething(unittest.TestCase): + def test_something(self) -> None: + self.assertEqual(1, 1) +