✨feat(cli): add cli features
add cli using typer, its option --input-dir, --input-files and --output-dir, add related tests, add logging, add and update of the package version in `logwatcher/__init__.py`, correct templates in `reporter.py`
This commit is contained in:
+4
-2
@@ -1,10 +1,12 @@
|
||||
[project]
|
||||
name = "logwatcher"
|
||||
version = "0.1.0"
|
||||
version = "0.0.1"
|
||||
description = "Trier, classifier et extraire automatiquement les entrées pertinentes d'un fichier de logs applicatif."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
"typer>=0.27.2",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from importlib.metadata import version
|
||||
|
||||
from logwatcher.config import *
|
||||
from logwatcher.logging_config import setup_logging
|
||||
|
||||
__version__ = version("logwatcher")
|
||||
logger = setup_logging()
|
||||
|
||||
+106
-3
@@ -1,9 +1,112 @@
|
||||
from logwatcher import logger
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
|
||||
from logwatcher import __version__, logger
|
||||
from logwatcher.classifier import classify_log_entries
|
||||
from logwatcher.models import LogEntry
|
||||
from logwatcher.parser import parse_log_file
|
||||
from logwatcher.reporter import write_log_report
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
def app():
|
||||
def version_callback(value: bool):
|
||||
"""
|
||||
Print the version of package.
|
||||
Stop the CLI after printing version.
|
||||
:return:
|
||||
"""
|
||||
if value:
|
||||
print(f"logwatcher version: {__version__}")
|
||||
raise typer.Exit()
|
||||
|
||||
|
||||
def _is_log_file(path: Path) -> bool:
|
||||
"""
|
||||
Return True if file is a log file (.txt, .log, or extensionless).
|
||||
:param path: Path to log file
|
||||
"""
|
||||
if not path.is_file():
|
||||
return False
|
||||
return path.suffix in {"", ".txt", ".log"}
|
||||
|
||||
|
||||
def _get_files_from_dir(input_dir: Path) -> list[Path]:
|
||||
if not input_dir.exists():
|
||||
raise FileNotFoundError(f"No such directory at '{input_dir}'")
|
||||
return [file for file in input_dir.iterdir() if _is_log_file(file)]
|
||||
|
||||
|
||||
def _files_to_log_entries(input_files: list[Path]) -> list[LogEntry]:
|
||||
log_entries = []
|
||||
for file in input_files:
|
||||
if not file.is_file():
|
||||
raise ValueError(f"'{file}' is not a file")
|
||||
log_entries.extend(parse_log_file(file))
|
||||
return log_entries
|
||||
|
||||
|
||||
@app.command()
|
||||
def cli(
|
||||
output_dir: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
"--output-dir", help="Output directories where reports will be stored."
|
||||
),
|
||||
] = Path("output"),
|
||||
input_files: Annotated[
|
||||
list[Path] | None, typer.Option("--input-files", help="Log files to scan.")
|
||||
] = None,
|
||||
input_dir: Annotated[
|
||||
Path | None,
|
||||
typer.Option("--input-dir", help="Directories containing log files."),
|
||||
] = None,
|
||||
version: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--version",
|
||||
help="Get the version of the package",
|
||||
callback=version_callback,
|
||||
is_eager=True,
|
||||
),
|
||||
] = False,
|
||||
) -> None:
|
||||
"""
|
||||
Entry point for logwatcher.
|
||||
|
||||
Convert log files into two reports, one of n2 related logs, one without any n2 related logs.
|
||||
These reports are sent by mail to n2 support technicians.
|
||||
These log entries are used to generate incidents on Ivanti using its API.
|
||||
|
||||
Args:
|
||||
input_files: Log files to scan. Either input_files or input_dir must be provided.
|
||||
input_dir: Directories containing log files. Either input_files or input_dir must be provided.
|
||||
output_dir: Required. Output directories where reports will be stored.
|
||||
version: Option that returns the version of package
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
logger.info("logwatcher starts its work.")
|
||||
print("hello, world!")
|
||||
if input_files and input_dir:
|
||||
raise typer.BadParameter(
|
||||
"Only one of input_files or input_dir must be provided."
|
||||
)
|
||||
|
||||
if input_files:
|
||||
logger.info(f"input_files option selected.\n input_files: '{input_files}'")
|
||||
log_files = input_files
|
||||
elif input_dir:
|
||||
logger.info(f"input_dir option selected with value '{input_dir}'")
|
||||
log_files = _get_files_from_dir(input_dir)
|
||||
else:
|
||||
raise typer.BadParameter("Either input_files or input_dir must be provided")
|
||||
|
||||
log_entries = _files_to_log_entries(log_files)
|
||||
relevant, irrelevant = classify_log_entries(log_entries)
|
||||
write_log_report(relevant, irrelevant, len(log_files), output_dir)
|
||||
logger.info("logwatcher has finished its task.")
|
||||
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ BASE_TEMPLATE = Template("""
|
||||
RAPPORT D'ANALYSE DE LOGS
|
||||
=========================
|
||||
Période\t : $start_date -> $end_date
|
||||
Fichiers lu\t : $nb_files
|
||||
Nombre total d'erreurs\t: $nb_errors
|
||||
Fichier(s) lu(s)\t : $nb_files
|
||||
Nombre total d'erreur(s)\t: $nb_errors
|
||||
$sub_content
|
||||
""")
|
||||
|
||||
@@ -19,7 +19,7 @@ N2_SUPPORT_TEMPLATE = Template("""
|
||||
=========================
|
||||
ERREURS SUPPORT N2
|
||||
=========================
|
||||
Nombre d'erreurs\t : $nb_errors
|
||||
Nombre d'erreur(s)\t : $nb_errors
|
||||
$error_list
|
||||
""")
|
||||
|
||||
@@ -27,7 +27,7 @@ OTHER_TEMPLATE = Template("""
|
||||
=========================
|
||||
AUTRES ERREURS
|
||||
=========================
|
||||
Nombre d'erreurs\t : $nb_errors
|
||||
Nombre d'erreur(s)\t : $nb_errors
|
||||
$error_list
|
||||
""")
|
||||
|
||||
@@ -35,7 +35,7 @@ ERROR_TEMPLATE = Template("""
|
||||
[$index] $error_name
|
||||
\tMagasin: $store_name
|
||||
\tMDC: $mdc
|
||||
\tServeur: $server_ip
|
||||
\tIP du MDC: $server_ip
|
||||
\tHeure: $error_time
|
||||
\tRaison: $error_message
|
||||
""")
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from logwatcher import __version__
|
||||
from logwatcher.cli import _get_files_from_dir, _is_log_file, app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
@pytest.fixture(name="output_path")
|
||||
def get_output_path_fixture(tmp_path: Path):
|
||||
"""
|
||||
Fixture that return output directory path where all logs and output files
|
||||
will be located during tests.
|
||||
|
||||
Args:
|
||||
tmp_path: Temporary root path for testing results
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
return tmp_path / "output"
|
||||
|
||||
|
||||
def get_argument_from_files(log_dir: Path, output_path: Path) -> list[str]:
|
||||
"""
|
||||
Utility function that implement an argument list to call cli
|
||||
Args:
|
||||
log_dir: Path to the logwatcher directory
|
||||
output_path: Temporary path for all output generated during testing
|
||||
|
||||
Returns:
|
||||
List of arguments in string format
|
||||
|
||||
"""
|
||||
files = [
|
||||
str(file) for file in log_dir.iterdir() if file.suffix in ("", ".txt", ".log")
|
||||
]
|
||||
|
||||
arguments = ["--output-dir", str(output_path)]
|
||||
for file in files:
|
||||
arguments += ["--input-files", file]
|
||||
return arguments
|
||||
|
||||
|
||||
def test_cli_help():
|
||||
"""
|
||||
Test the help command and assert that all options are listed
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
results = runner.invoke(app, ["--help"])
|
||||
assert results.exit_code == 0
|
||||
assert "Usage: cli [OPTIONS]" in results.output
|
||||
assert "--input-dir" in results.output
|
||||
assert "--input-files" in results.output
|
||||
assert "--output-dir" in results.output
|
||||
assert "--version" in results.output
|
||||
|
||||
|
||||
def test_version_cli():
|
||||
"""
|
||||
Test the version option. The version must be printed
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
result = runner.invoke(app, ["--version"])
|
||||
assert result.exit_code == 0
|
||||
assert f"logwatcher version: {__version__}" in result.output
|
||||
|
||||
|
||||
def test_is_log_file(tmp_path: Path):
|
||||
"""
|
||||
Assert only .log, .txt or file without extensions are valid
|
||||
|
||||
Args:
|
||||
tmp_path: Temporary root path for testing results
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# set up files
|
||||
Path(tmp_path / "a.log").touch()
|
||||
Path(tmp_path / "a.txt").touch()
|
||||
Path(tmp_path / "a.csv").touch()
|
||||
Path(tmp_path / "a.py").touch()
|
||||
Path(tmp_path / "a").touch()
|
||||
Path(tmp_path / "a.pdf").touch()
|
||||
|
||||
# test
|
||||
assert _is_log_file(Path(tmp_path / "a.log"))
|
||||
assert _is_log_file(Path(tmp_path / "a.txt"))
|
||||
assert _is_log_file(Path(tmp_path / "a")) # without extensions
|
||||
assert not _is_log_file(Path(tmp_path / "a.csv"))
|
||||
assert not _is_log_file(Path(tmp_path / "a.py"))
|
||||
assert not _is_log_file(Path(tmp_path / "a.pdf"))
|
||||
|
||||
|
||||
def test_get_files_from_dir_empty_dir(tmp_path: Path):
|
||||
"""
|
||||
Assert that a non-existing directory (or wrong path) cannot be
|
||||
given as a working input directory.
|
||||
Assert that everything works even if the directory is empty.
|
||||
|
||||
Args:
|
||||
tmp_path: Temporary root path for testing results
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
with pytest.raises(FileNotFoundError):
|
||||
_get_files_from_dir(tmp_path / "input_test")
|
||||
input_test_dir = tmp_path / "input_test"
|
||||
input_test_dir.mkdir()
|
||||
assert input_test_dir.is_dir()
|
||||
assert _get_files_from_dir(input_test_dir) == []
|
||||
|
||||
|
||||
def test_get_files_from_dir_not_empty_dir(tmp_path: Path):
|
||||
"""
|
||||
Assert the right files are retrieved if the directory is not empty.
|
||||
|
||||
Args:
|
||||
tmp_path: Temporary root path for testing results
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# create directory
|
||||
input_test_dir = tmp_path / "input_test"
|
||||
input_test_dir.mkdir()
|
||||
# populate it with files
|
||||
Path(input_test_dir / "a.log").touch()
|
||||
Path(input_test_dir / "a.txt").touch()
|
||||
Path(input_test_dir / "a.csv").touch()
|
||||
Path(input_test_dir / "a.py").touch()
|
||||
Path(input_test_dir / "a").touch()
|
||||
Path(input_test_dir / "a.pdf").touch()
|
||||
|
||||
assert input_test_dir.is_dir()
|
||||
file_list = _get_files_from_dir(input_test_dir)
|
||||
assert file_list != []
|
||||
assert len(file_list) == 3
|
||||
assert {file.name for file in file_list} == {"a.log", "a.txt", "a"}
|
||||
|
||||
|
||||
def test_cli_input_files_and_input_dir(output_path: Path):
|
||||
"""
|
||||
Assert that input files and input dir options cannot be provided in the same command.
|
||||
|
||||
Args:
|
||||
tmp_path: Temporary root path for testing results
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
results = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"--input-files",
|
||||
"a.log",
|
||||
"--input-dir",
|
||||
"logs",
|
||||
"--output-dir",
|
||||
str(output_path),
|
||||
],
|
||||
)
|
||||
assert results.exit_code == 2
|
||||
assert "Only one of input_files or input_dir must be provided." in results.output
|
||||
assert not output_path.exists()
|
||||
|
||||
|
||||
def test_cli_neither_input_files_nor_input_dir():
|
||||
"""
|
||||
Test calling cli without providing input files or input dir options.
|
||||
Must return an error on provided parameters (BadParameter = exit code 2)
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
results = runner.invoke(app)
|
||||
assert results.exit_code == 2
|
||||
assert "Either input_files or input_dir must be provided" in results.output
|
||||
|
||||
|
||||
def test_cli_bad_input_file(tmp_path: Path, output_path: Path, invalid_log_dir: Path):
|
||||
"""
|
||||
Test calling cli with bad input file (directory instead of file)
|
||||
Must return an exit code at 1 and the ValueError exception raised
|
||||
while `_files_to_log_entries()` was running.
|
||||
|
||||
Args:
|
||||
tmp_path: Temporary root path for testing results
|
||||
output_path: Temporary path for all output generated during testing
|
||||
invalid_log_dir: Fixture directory of invalid logs
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
directory = str(tmp_path)
|
||||
results = runner.invoke(
|
||||
app,
|
||||
["--input-files", directory, "--output-dir", str(output_path)],
|
||||
)
|
||||
assert results.exit_code == 1
|
||||
assert isinstance(results.exception, ValueError)
|
||||
assert f"'{directory}' is not a file" in results.exception.args[0]
|
||||
assert (
|
||||
not output_path.exists()
|
||||
) # would be created if `directory` was a valid parameter
|
||||
|
||||
|
||||
def test_cli_empty_input_files(output_path: Path, invalid_log_dir: Path):
|
||||
"""
|
||||
Test calling cli with empty input files.
|
||||
Must pass and generate reports that mention reading a file only.
|
||||
All other parameters must be at 0.
|
||||
|
||||
Args:
|
||||
output_path: Temporary path for all output generated during testing
|
||||
invalid_log_dir: Fixture directory of invalid logs
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
empty_file = str(next(invalid_log_dir.glob("empty_file.txt")))
|
||||
results = runner.invoke(
|
||||
app,
|
||||
["--input-files", empty_file, "--output-dir", str(output_path)],
|
||||
)
|
||||
assert results.exit_code == 0
|
||||
# check files
|
||||
nb_files = 1 # only empty_file.txt should have been read
|
||||
file_list = list(output_path.iterdir())
|
||||
assert len(file_list) == 3
|
||||
for file in file_list:
|
||||
content = file.read_text(encoding="windows-1252")
|
||||
assert file.name in ("all.log", "n2.log", "other.log")
|
||||
assert f"Fichier(s) lu(s)\t : {nb_files}" in content
|
||||
assert "Nombre total d'erreur(s)\t: 0" in content
|
||||
|
||||
|
||||
def test_cli_input_files_or_input_dir(
|
||||
tmp_path: Path, output_path: Path, original_log_dir: Path
|
||||
):
|
||||
"""
|
||||
Assert that calling the CLI with either input files or input dir options
|
||||
is a valid process.
|
||||
|
||||
Args:
|
||||
tmp_path: Temporary root path for testing results
|
||||
output_path: Temporary path for all output generated during testing
|
||||
original_log_dir: Fixture directory of initial logs received by n2 technicians
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# output for input-files option
|
||||
input_files_output = output_path / "input_files"
|
||||
# save number of files from original log directory for future assertion test
|
||||
nb_files = len(list(original_log_dir.iterdir()))
|
||||
|
||||
# test with `input files` option provided only
|
||||
result = runner.invoke(
|
||||
app, get_argument_from_files(original_log_dir, input_files_output)
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert result.exception is None
|
||||
|
||||
output_file_list = list(input_files_output.iterdir())
|
||||
assert len(output_file_list) == 3
|
||||
# check files
|
||||
for file in output_file_list:
|
||||
assert file.name in ("all.log", "n2.log", "other.log")
|
||||
assert file.stat().st_size != 0 # must contain basic execution information
|
||||
assert f"Fichier(s) lu(s)\t : {nb_files}" in file.read_text(
|
||||
encoding="windows-1252"
|
||||
)
|
||||
|
||||
# test with `input dir` option provided only
|
||||
# output for input-dir option
|
||||
input_dir_output = output_path / "input_dir"
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"--input-dir",
|
||||
str(original_log_dir),
|
||||
"--output-dir",
|
||||
str(input_dir_output),
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert result.exception is None
|
||||
output_file_list = list(input_dir_output.iterdir())
|
||||
assert len(output_file_list) == 3
|
||||
# check files
|
||||
for file in output_file_list:
|
||||
assert file.name in ("all.log", "n2.log", "other.log")
|
||||
assert file.stat().st_size != 0 # must contain basic execution information
|
||||
assert f"Fichier(s) lu(s)\t : {nb_files}" in file.read_text(
|
||||
encoding="windows-1252"
|
||||
)
|
||||
|
||||
|
||||
def test_cli_input_files_one_file(valid_log_dir: Path, output_path: Path):
|
||||
mixed_logs_file = valid_log_dir / "mixed_logs.txt"
|
||||
result = runner.invoke(
|
||||
app, ["--input-files", str(mixed_logs_file), "--output-dir", str(output_path)]
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert result.exception is None
|
||||
output_file_list = list(output_path.iterdir())
|
||||
assert len(output_file_list) == 3
|
||||
# check file
|
||||
date_pattern = r"\d{1,2}/\d{1,2}/\d{4} \d{2}:\d{2}:\d{2}"
|
||||
for file in output_file_list:
|
||||
content = file.read_text(encoding="windows-1252")
|
||||
assert re.search(f"Période\t : {date_pattern} -> {date_pattern}\n", content)
|
||||
assert file.name in ("all.log", "n2.log", "other.log")
|
||||
assert "Fichier(s) lu(s)\t : 1" in file.read_text(encoding="windows-1252")
|
||||
assert "Nombre total d'erreur(s)\t: 0" not in content
|
||||
|
||||
|
||||
def test_cli_empty_input_directory(tmp_path: Path, output_path: Path):
|
||||
"""
|
||||
Assert that CLI can run on empty directory without error and
|
||||
generates reports that indicates no file read.
|
||||
|
||||
Args:
|
||||
tmp_path: Temporary root path for testing results
|
||||
output_path: Temporary path for all output generated during testing
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
input_dir = Path(tmp_path / "input_test")
|
||||
input_dir.mkdir(parents=True)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"--input-dir",
|
||||
str(input_dir),
|
||||
"--output-dir",
|
||||
str(output_path),
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
output_file_list = list(output_path.iterdir())
|
||||
assert len(output_file_list) == 3
|
||||
# check files
|
||||
for file in output_file_list:
|
||||
content = file.read_text(encoding="windows-1252")
|
||||
assert file.name in ("all.log", "n2.log", "other.log")
|
||||
assert file.stat().st_size != 0 # must contain basic execution information
|
||||
assert "Période : -> " in content
|
||||
assert "Fichier(s) lu(s)\t : 0" in content
|
||||
assert "Nombre total d'erreur(s)\t: 0" in content
|
||||
|
||||
|
||||
def test_cli_invalid_logs(output_path: Path, invalid_log_dir: Path):
|
||||
"""
|
||||
On invalid logs (empty files, log from different sources than MDC LAME...),
|
||||
CLI should run without error and creates report files with metrics at 0.
|
||||
|
||||
Args:
|
||||
output_path: Temporary path for all output generated during testing
|
||||
invalid_log_dir: Fixture directory of invalid logs
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
arguments = get_argument_from_files(invalid_log_dir, output_path)
|
||||
results = runner.invoke(app, arguments)
|
||||
|
||||
assert results.exit_code == 0
|
||||
assert output_path.exists()
|
||||
|
||||
nb_files = len(list(invalid_log_dir.iterdir()))
|
||||
output_file_list = list(output_path.iterdir())
|
||||
|
||||
assert len(output_file_list) == 3
|
||||
for file in output_file_list:
|
||||
content = file.read_text(encoding="windows-1252")
|
||||
assert "Période : -> " in content
|
||||
assert f"Fichier(s) lu(s)\t : {nb_files}" in content
|
||||
assert "Nombre total d'erreur(s)\t: 0" in content
|
||||
|
||||
|
||||
def test_cli_valid_logs(output_path: Path, valid_log_dir: Path):
|
||||
"""
|
||||
On valid test, everything should work successfully. Metrics
|
||||
should not be at 0.
|
||||
|
||||
Args:
|
||||
output_path: Temporary path for all output generated during testing
|
||||
valid_log_dir: Fixture directory of valid logs
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
arguments = get_argument_from_files(valid_log_dir, output_path)
|
||||
results = runner.invoke(app, arguments)
|
||||
|
||||
assert results.exit_code == 0
|
||||
assert output_path.exists()
|
||||
|
||||
nb_files = len(list(valid_log_dir.iterdir()))
|
||||
date_pattern = r"\d{1,2}/\d{1,2}/\d{4} \d{2}:\d{2}:\d{2}"
|
||||
output_file_list = list(output_path.iterdir())
|
||||
|
||||
assert len(output_file_list) == 3
|
||||
for file in output_file_list:
|
||||
content = file.read_text(encoding="windows-1252")
|
||||
assert re.search(f"Période\t : {date_pattern} -> {date_pattern}\n", content)
|
||||
assert f"Fichier(s) lu(s)\t : {nb_files}" in content
|
||||
assert "Nombre total d'erreur(s)\t: 0" not in content
|
||||
@@ -6,6 +6,15 @@ resolution-markers = [
|
||||
"python_full_version < '3.15'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ast-serialize"
|
||||
version = "0.6.0"
|
||||
@@ -299,8 +308,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "logwatcher"
|
||||
version = "0.1.0"
|
||||
version = "0.0.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
@@ -311,6 +323,7 @@ dev = [
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "typer", specifier = ">=0.27.2" }]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
@@ -320,6 +333,27 @@ dev = [
|
||||
{ name = "ruff", specifier = ">=0.16.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "2.3.0"
|
||||
@@ -457,6 +491,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "15.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.16.0"
|
||||
@@ -482,6 +529,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.4.1"
|
||||
@@ -536,6 +592,21 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.27.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/bf/205d0004930ede8f542fb58f601526fccf4ae7626075ca1e6c4de5d3d652/typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d", size = 123130, upload-time = "2026-08-28T10:26:53.752Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.16.0"
|
||||
|
||||
Reference in New Issue
Block a user