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
133 changes: 58 additions & 75 deletions Jenkinsfile

Large diffs are not rendered by default.

56 changes: 51 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,27 @@ documentation = "https://uiuclibrary.github.io/tripwire/"
tripwire = "uiucprescon.tripwire.main:main"

[dependency-groups]
test = ["pytest>7", "coverage"]
test = ["pytest>7"]
docs = ["sphinx"]
audit-dependencies = ['uv-secure']
type-checking = ["mypy", "types-tqdm"]
lint = ["ruff"]
tox = ["tox"]
tox-uv = [
{include-group = "tox"},
"tox-uv-bare"
]
dev = [
"lxml",
"mypy",
"pre-commit",
"pytest",
"ruff",
"tox",
"types-tqdm",
{include-group = "audit-dependencies"},
{include-group = "docs"},
{include-group = "lint"},
{include-group = "test"},
{include-group = "tox"},
{include-group = "type-checking"},
"coverage"
]
deploy = ["twine"]
ci = ["pysonar", {include-group = "dev"}]
Expand Down Expand Up @@ -68,6 +75,7 @@ pre_bump_hooks = [
[tool.ruff]
line-length = 79
extend-exclude = ["tests"]
include = ["src/**/*.py"]

[tool.ruff.lint]
extend-select = [
Expand All @@ -90,6 +98,44 @@ description = "Run test under {base_python}"
commands = [["{env_bin_dir}{/}pytest"]]
dependency_groups = ['test']

[tool.tox.env.type-checking]
description = "Run mypy type checking"
dependency_groups = ['type-checking']
commands = [["{env_bin_dir}{/}mypy", "-p", "uiucprescon.tripwire"]]

[tool.tox.env.lint]
description = "Run ruff linting"
dependency_groups = ['lint']
skip_install = true
commands = [["{env_bin_dir}{/}ruff", "check"]]

[tool.tox.env.docs]
description = "Build documentation"
dependency_groups = ['docs']
commands = [
[
"{env_bin_dir}{/}sphinx-build", "docs", "{temp_dir}{/}build{/}docs",
"--fail-on-warning",
"--keep-going",
"--write-all",
"--fresh-env"
]
]

[tool.tox.env.docs-linkcheck]
description = "Build documentation"
dependency_groups = ['docs']
commands = [
[
"{env_bin_dir}{/}sphinx-build", "docs", "{temp_dir}{/}build{/}docs-link-check",
"--fail-on-warning",
"--keep-going",
"--write-all",
"--fresh-env",
"--builder=linkcheck"
]
]

[tool.mypy]
mypy_path = "src"

Expand Down
35 changes: 35 additions & 0 deletions src/uiucprescon/tripwire/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Exceptions used in the tripwire package.

.. versionadded:: 0.3.7
Added Exceptions module and relocated InvalidFileFormat to here
"""


class TripwireException(Exception):
"""Base class for all exceptions raised by this module."""


class InvalidFileFormat(TripwireException):
"""Invalid file format exception.

.. versionchanged:: 0.3.7
relocate from uiucprescon.tripwire.files to here
"""

def __init__(self, file: str = "", details: str = "") -> None:
"""Initialize exception.

Args:
file: path of the file that caused the exception. Optional.
details: details of the exception. Optional.
"""
message = (
f"Invalid file format. File: {file}"
if file
else "Invalid file format"
)
if details:
message = f"{message}. Details: {details}"
super().__init__(message)
self.file_name = file
self.details = details
23 changes: 6 additions & 17 deletions src/uiucprescon/tripwire/files.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
"""File handling for files used by tripwire."""
"""File handling for files used by tripwire.

.. versionchanged:: 0.3.7
InvalidFileFormat now located in uiucprescon.tripwire.exceptions

"""

import abc
import collections
Expand Down Expand Up @@ -35,22 +40,6 @@
logger.setLevel(logging.INFO)


class InvalidFileFormat(Exception):
"""Invalid file format exception."""

def __init__(self, file: str = "", details: str = "") -> None:
message = (
f"Invalid file format. File: {file}"
if file
else "Invalid file format"
)
if details:
message = f"{message}. Details: {details}"
super().__init__(message)
self.file_name = file
self.details = details


@dataclasses.dataclass(frozen=True)
class TableRow:
"""Table row."""
Expand Down
34 changes: 34 additions & 0 deletions src/uiucprescon/tripwire/introspection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Introspection utilities."""

from importlib import metadata

__all__ = ["get_application_info"]


def get_application_info() -> str:
"""Get application info."""
description = get_application_description()
packages_report = (
f"\nInstalled Python packages:\n\n{get_install_packages_info()}"
)
return "\n".join(list(filter(None, [description, packages_report])))


def get_application_description() -> str:
return (
"Tripwire is a tool for validating and processing media manifests. "
"It is developed by the University of Illinois Urbana-Champaign "
"Library's Preservation Services Department."
)


def get_install_packages_info() -> str:
return "\n".join(
[
f" {x.metadata['Name']}, version: {x.metadata['Version']}"
for x in sorted(
metadata.distributions(),
key=lambda x: x.metadata["Name"].upper(),
)
]
)
86 changes: 77 additions & 9 deletions src/uiucprescon/tripwire/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@
# PYTHON_ARGCOMPLETE_OK

import argparse
import contextlib
import functools
import logging
import pathlib
import sys
from typing import Callable, Any, Dict, Tuple, Optional

from uiucprescon.tripwire import validation, utils, manifest_check, metadata
from uiucprescon.tripwire.files import InvalidFileFormat
from uiucprescon.tripwire import (
validation,
utils,
manifest_check,
metadata,
introspection,
)
from uiucprescon.tripwire.exceptions import InvalidFileFormat
import argcomplete

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -121,10 +128,22 @@ def get_arg_parser() -> Tuple[

metadata_show_show.add_argument("glob", type=str)

metadata_validate = metadata_parser.add_parser("validate")
metadata_validate = metadata_parser.add_parser(
"validate", help="validate files from mediaconch policy"
)
metadata_validate.add_argument("policy_file", type=pathlib.Path)
metadata_validate.add_argument("glob", type=str)

metadata_validate.add_argument(
"-v",
"--verbose",
action="count",
default=1,
help="increase output verbosity",
dest="verbosity",
)
sub_commands.add_parser(
"info", help="get information about current version of tripwire"
)
return (
parser,
{
Expand All @@ -142,14 +161,56 @@ def metadata_show_command(args: argparse.Namespace) -> None:
metadata.show_metadata(args.glob, search_path=pathlib.Path("."))


@contextlib.contextmanager
def module_logging_verbosity(logger, verbosity=logging.INFO):
"""Set logging level to verbosity for a module.

This is useful for setting the logging level for a module to a specific
level for the duration of a block of code, and then resetting it back after
leaving the decorated function's scope.
"""
starting_verbosity = logger.getEffectiveLevel()
handler_levels = {}
try:
logger.setLevel(verbosity)
for handler in logger.handlers:
handler_levels[handler.name] = handler.level
handler.setLevel(verbosity)
yield
finally:
logger.setLevel(starting_verbosity)
for handler in logger.handlers:
if handler.name in handler_levels:
handler.setLevel(handler_levels[handler.name])


@capture_log(logger=metadata.logger)
def metadata_validate_command(args: argparse.Namespace) -> None:
def metadata_validate_command(
args: argparse.Namespace,
validate_metadata_strategy=metadata.validate_metadata,
) -> None:
"""Run metadata validate command."""
if not metadata.validate_metadata(
args.glob, policy_xml_file=args.policy_file

def get_log_level(verbosity: int) -> int:
if verbosity > 2:
print("verbosity level to max, defaulting to DEBUG")
return logging.DEBUG
else:
match args.verbosity:
case 1:
return logging.INFO
case 2:
return logging.DEBUG
return logging.INFO

with module_logging_verbosity(
metadata.logger, verbosity=get_log_level(args.verbosity)
):
print("failed metadata validation")
sys.exit(1)
if not validate_metadata_strategy(
args.glob, policy_xml_file=args.policy_file
):
print("failed metadata validation")
sys.exit(1)

print("passed metadata validation")

Expand All @@ -165,6 +226,11 @@ def metadata_command(args: argparse.Namespace, subcommand: str) -> None:
raise ValueError(f"Unknown metadata subcommand: {subcommand}")


def show_info_command() -> None:
"""Show info about application."""
print(introspection.get_application_info())


def main() -> None:
"""Main entry point for the Tripwire command line interface."""
parser, print_help_commands = get_arg_parser()
Expand All @@ -182,6 +248,8 @@ def main() -> None:
)
case "metadata":
metadata_command(args, args.metadata_command)
case "info":
show_info_command()


if __name__ == "__main__":
Expand Down
10 changes: 5 additions & 5 deletions src/uiucprescon/tripwire/manifest_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
)
import logging
from uiucprescon.tripwire import files as tripwire_files
from uiucprescon.tripwire.exceptions import InvalidFileFormat

from tqdm import tqdm

__all__ = ["locate_manifest_files"]
Expand Down Expand Up @@ -349,9 +351,7 @@ def locate_manifest_files_fp(
)
manifest = tripwire_files.TSVManifest(manifest_tsv_fp)
if not manifest.is_valid_file():
raise tripwire_files.InvalidFileFormat(
details="Not a valid TSV manifest file."
)
raise InvalidFileFormat(details="Not a valid TSV manifest file.")

scanner = PackageScanner(search_path)
for row in (
Expand Down Expand Up @@ -389,8 +389,8 @@ def locate_manifest_files(
unexpected_files = locate_manifest_files_fp(
fp, search_path, manifest_type=manifest_type
)
except tripwire_files.InvalidFileFormat as e:
raise tripwire_files.InvalidFileFormat(
except InvalidFileFormat as e:
raise InvalidFileFormat(
file=manifest_tsv.name, details=e.details
) from e
if unexpected_files:
Expand Down
Loading
Loading