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)
+