Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Run Tests

on:
- push
- pull_request

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"

steps:
- uses: actions/checkout@v6
- name: Set up git-annex
run: |
sudo apt-get update
sudo apt-get install -y git-annex
- name: Install Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
python -m pip install tox tox-gh-actions
- name: Configure Git for DataLad
run: |
git config --global user.email "julio-runner@github.com"
git config --global user.name "GitHub Runner"
- name: Test with tox
run: |
tox
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
fail_ci_if_error: true
flags: julio
if: success() && matrix.python-version == 3.13
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## [0.0.1](https://github.com/juaml/junifer-julio/tree/0.0.1) - 2025-12-11

### Added

- Introduce registry creation ([#2](https://github.com/juaml/juni-julio/issues/2))


## [0.0.0](https://github.com/juaml/junifer-julio/tree/0.0.0) - 2025-12-11

### Added
Expand Down
29 changes: 29 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
codecov:
notify: {}
require_ci_to_pass: false

comment: # this is a top-level key
layout: "reach, diff, flags, files"
behavior: default
require_changes: false # if true: only post the comment if coverage changes
require_base: no # [yes :: must have a base report to post]

coverage:
status:
project: off
patch: off

flag_management:
default_rules:
carryforward: true
individual_flags:
- name: julio
paths:
- julio/
statuses:
- type: project
target: 90%
threshold: 1%
- type: patch
target: 95%
threshold: 1%
5 changes: 5 additions & 0 deletions julio/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
__all__ = [
"create",
]

from ._functions import create
154 changes: 154 additions & 0 deletions julio/_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""CLI for julio."""

# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
# License: AGPL

import logging
import logging.config
import pathlib
import sys

import click
import datalad
import structlog

from . import _functions as cli_func


__all__ = ["cli", "create"]

# Common processors for stdlib and structlog
_timestamper = structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S")


def _remove_datalad_message(_, __, event_dict):
"""Clean datalad records."""
if "message" in event_dict:
event_dict.pop("message")
if "dlm_progress" in event_dict:
event_dict.pop("dlm_progress")
if "dlm_progress_noninteractive_level" in event_dict:
event_dict.pop("dlm_progress_noninteractive_level")
if "dlm_progress_update" in event_dict:
event_dict.pop("dlm_progress_update")
if "dlm_progress_label" in event_dict:
event_dict.pop("dlm_progress_label")
if "dlm_progress_unit" in event_dict:
event_dict.pop("dlm_progress_unit")
if "dlm_progress_total" in event_dict:
event_dict.pop("dlm_progress_total")
return event_dict


_pre_chain = [
structlog.stdlib.add_log_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.ExtraAdder(),
_timestamper,
_remove_datalad_message,
]


def _set_log_config(verbose: int) -> None:
"""Set logging config.

Parameters
----------
verbose : int
Verbosity.

"""
# Configure logger based on verbosity
if verbose == 0:
level = logging.WARNING
elif verbose == 1:
level = logging.INFO
else:
level = logging.DEBUG
# Configure stdlib
logging.config.dictConfig(
{
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"console": {
"()": structlog.stdlib.ProcessorFormatter,
"processors": [
structlog.stdlib.add_logger_name,
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
structlog.dev.ConsoleRenderer(
colors=sys.stdout.isatty() and sys.stderr.isatty()
),
],
"foreign_pre_chain": _pre_chain,
},
},
"handlers": {
"default": {
"level": level,
"class": "logging.StreamHandler",
"formatter": "console",
},
},
"loggers": {
"": {
"handlers": ["default"],
"level": level,
"propagate": True,
},
},
}
)
# Configure structlog
structlog.configure(
processors=[
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.make_filtering_bound_logger(level),
cache_logger_on_first_use=True,
)
# Remove datalad logger handlers to avoid duplicate logging
_datalad_lgr_hdlrs = datalad.log.lgr.handlers
for h in _datalad_lgr_hdlrs:
datalad.log.lgr.removeHandler(h)
datalad.log.lgr.setLevel(level)


@click.group
@click.version_option(prog_name="julio")
@click.help_option()
def cli() -> None:
"""julio CLI.""" # noqa: D403


@cli.command
@click.argument(
"registry_path",
type=click.Path(
exists=False,
readable=True,
writable=True,
file_okay=False,
path_type=pathlib.Path,
),
metavar="<registry>",
)
@click.option("-v", "--verbose", count=True, type=int)
def create(
registry_path: click.Path,
verbose: int,
) -> None:
"""Create registry."""
_set_log_config(verbose)
try:
cli_func.create(registry_path)
except RuntimeError as err:
click.echo(f"{err}", err=True)
else:
click.echo("Success")
56 changes: 56 additions & 0 deletions julio/_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Functions for julio."""

# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
# License: AGPL

from pathlib import Path

import datalad.api as dl
import structlog
from datalad.support.exceptions import IncompleteResultsError


__all__ = ["create"]


logger = structlog.get_logger()


def create(registry_path: Path):
"""Create a registry at `registry_path`.

Parameters
----------
registry_path : pathlib.Path
Path to the registry.

Raises
------
RuntimeError
If there is a problem creating the registry.

"""
try:
ds = dl.create(
path=registry_path,
cfg_proc="text2git",
on_failure="stop",
result_renderer="disabled",
)
except IncompleteResultsError as e:
raise RuntimeError(f"Failed to create dataset: {e.failed}") from e
else:
logger.debug(
"Registry created successfully",
cmd="create",
path=str(registry_path.resolve()),
)
# Add config file
conf_path = Path(ds.path) / "registry-config.yml"
conf_path.touch()
ds.save(
conf_path,
message="[julio] add registry configuration",
on_failure="stop",
result_renderer="disabled",
)
29 changes: 29 additions & 0 deletions julio/tests/test_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Test for functions."""

# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
# License: AGPL

import shutil
from pathlib import Path

import pytest

from julio import create


def test_create(tmp_path: Path) -> None:
"""Test registry creation.

Parameters
----------
tmp_path : Path
Pytest fixture that provides a temporary directory.

"""
registry_path = tmp_path / "test_registry"
create(registry_path)
config_path = registry_path / "registry-config.yml"
assert config_path.is_file()
with pytest.raises(RuntimeError):
create(registry_path)
shutil.rmtree(registry_path)
30 changes: 29 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies = [
"click>=8.1.3,<8.2",
"datalad>=1.0.0,<1.3.0",
"lazy_loader==0.4",
"structlog>=25.0.0,<26.0.0",
]
dynamic = ["version"]

Expand Down Expand Up @@ -123,12 +124,13 @@ known-first-party = ["julio"]
known-third-party =[
"click",
"datalad",
"structlog",
"pytest",
]

[tool.pytest.ini_options]
minversion = "7.0"
testpaths = "tests"
testpaths = "julio"
log_cli_level = "INFO"
xfail_strict = true
addopts = [
Expand Down Expand Up @@ -176,3 +178,29 @@ showcontent = true
directory = "fixed"
name = "Fixed"
showcontent = true

[tool.coverage.paths]
source = [
"julio",
"*/site-packages/julio",
]

[tool.coverage.run]
branch = true
omit = [
"*/__init__.py",
"*/_version.py",
"*/_cli.py",
"*/tests/*",
]

[tool.coverage.report]
exclude_lines = [
# Have to re-enable the standard pragma
"pragma: no cover",
# Type checking if statements should not be considered
"if TYPE_CHECKING:",
# Don't complain if non-runnable code isn't run:
"if __name__ == .__main__.:",
]
precision = 2
Loading