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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies = [
"python-dotenv",
"typer[all]",
"rich",
"pyyaml",
]

[project.optional-dependencies]
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ lxml
python-dotenv
typer[all]
rich
pyyaml

# dev dependencies
pytest
Expand Down
16 changes: 9 additions & 7 deletions src/py_moodle/cli/categories.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""Category management commands for ``py-moodle``."""

import json

import typer
from rich.console import Console
from rich.table import Table
Expand All @@ -12,6 +10,7 @@
delete_category,
list_categories,
)
from py_moodle.cli.output import OutputFormat, emit
from py_moodle.session import MoodleSession

app = typer.Typer(help="Manage course categories: list, create, delete.")
Expand All @@ -30,7 +29,9 @@ def main(ctx: typer.Context):
@app.command("list")
def list_all_categories(
ctx: typer.Context,
json_flag: bool = typer.Option(False, "--json", help="Output in JSON format."),
output: OutputFormat = typer.Option(
OutputFormat.TABLE, "--output", help="Output format: table, json, or yaml."
),
):
"""
Lists all available course categories.
Expand All @@ -46,18 +47,19 @@ def list_all_categories(

try:
categories = list_categories(ms.session, ms.settings.url, ms.token)
if json_flag:
typer.echo(json.dumps(categories, indent=2, ensure_ascii=False))
else:

def _render_table(data):
table = Table("ID", "Name", "Parent ID", "Course Count")
for category in categories:
for category in data:
table.add_row(
str(category.get("id", "")),
category.get("name", ""),
str(category.get("parent", "")),
str(category.get("coursecount", "")),
)
Console().print(table)

emit(categories, output, table_fn=_render_table)
except MoodleCategoryError as e:
typer.echo(f"Error listing categories: {e}", err=True)
raise typer.Exit(1)
Expand Down
24 changes: 11 additions & 13 deletions src/py_moodle/cli/courses.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
"""Course-related commands for ``py-moodle``."""

import json

import typer
from rich.console import Console
from rich.table import Table

from py_moodle.cli.output import OutputFormat, emit
from py_moodle.course import (
MoodleCourseError,
create_course,
Expand All @@ -32,8 +31,8 @@ def main(ctx: typer.Context):
@app.command("list")
def list_all_courses(
ctx: typer.Context,
json_flag: bool = typer.Option(
False, "--json", help="Display output in JSON format."
output: OutputFormat = typer.Option(
OutputFormat.TABLE, "--output", help="Output format: table, json, or yaml."
),
):
"""
Expand All @@ -44,11 +43,9 @@ def list_all_courses(
ms.session, ms.settings.url, token=ms.token, sesskey=ms.sesskey
)

if json_flag:
typer.echo(json.dumps(courses, indent=2, ensure_ascii=False))
else:
def _render_table(data):
table = Table("ID", "Shortname", "Fullname", "Category", "Visible")
for course in courses:
for course in data:
table.add_row(
str(course.get("id", "")),
course.get("shortname", ""),
Expand All @@ -58,6 +55,8 @@ def list_all_courses(
)
Console().print(table)

emit(courses, output, table_fn=_render_table)


def _print_course_summary_table(course_data: dict):
"""Prints a rich summary table of the course contents."""
Expand Down Expand Up @@ -99,7 +98,9 @@ def _print_course_summary_table(course_data: dict):
def show_course(
ctx: typer.Context,
course_id: int = typer.Argument(..., help="ID of the course to show."),
json_flag: bool = typer.Option(False, "--json", help="Output in JSON format."),
output: OutputFormat = typer.Option(
OutputFormat.TABLE, "--output", help="Output format: table, json, or yaml."
),
):
"""
Shows a detailed summary of a specific course, including its sections and modules.
Expand All @@ -111,10 +112,7 @@ def show_course(
ms.session, ms.settings.url, ms.sesskey, course_id, token=ms.token
)

if json_flag:
typer.echo(json.dumps(course_data, indent=2, ensure_ascii=False))
else:
_print_course_summary_table(course_data)
emit(course_data, output, table_fn=_print_course_summary_table)

except MoodleCourseError as e:
typer.echo(f"Error getting course details: {e}", err=True)
Expand Down
28 changes: 16 additions & 12 deletions src/py_moodle/cli/modules.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""Module-related commands for ``py-moodle``."""

import json
from typing import Optional

import typer

from py_moodle.assign import MoodleAssignError, add_assign
from py_moodle.cli.output import OutputFormat, emit
from py_moodle.label import MoodleLabelError, add_label, update_label

# Import functions from the library directly
Expand Down Expand Up @@ -64,7 +64,9 @@ def delete_a_module(
def show_a_module(
ctx: typer.Context,
cmid: int = typer.Argument(..., help="ID of the module (cmid) to show."),
json_flag: bool = typer.Option(False, "--json", help="Output in JSON format."),
output: OutputFormat = typer.Option(
OutputFormat.TABLE, "--output", help="Output format: table, json, or yaml."
),
):
"""
Shows detailed information for a specific module.
Expand All @@ -73,11 +75,11 @@ def show_a_module(
try:
# This function is also generic and doesn't need changes.
module_info = get_module_info(ms.session, ms.settings.url, ms.sesskey, cmid)
if json_flag:
typer.echo(json.dumps(module_info, indent=2, ensure_ascii=False))
else:
table_str = format_module_table(module_info)
typer.echo(table_str)

def _render_table(data):
typer.echo(format_module_table(data))

emit(module_info, output, table_fn=_render_table)

except MoodleModuleError as e:
typer.echo(f"Error getting module info: {e}", err=True)
Expand Down Expand Up @@ -256,7 +258,9 @@ def list_available_module_types(
"--course-id",
help="Course ID to check available modules for. Defaults to 1.",
),
json_flag: bool = typer.Option(False, "--json", help="Output in JSON format."),
output: OutputFormat = typer.Option(
OutputFormat.TABLE, "--output", help="Output format: table, json, or yaml."
),
):
"""
Lists all available module types (activities/resources) that can be added to a course.
Expand All @@ -273,9 +277,7 @@ def list_available_module_types(
ms.session, ms.settings.url, ms.sesskey, course_id
)

if json_flag:
typer.echo(json.dumps(module_types, indent=2, ensure_ascii=False))
else:
def _render_table(data):
table = Table(
title=f"Available Module Types in Course ID {course_id}",
show_header=True,
Expand All @@ -285,7 +287,7 @@ def list_available_module_types(
table.add_column("Name (modname)", width=20)
table.add_column("Title (Translated)", justify="left")

for module in module_types:
for module in data:
table.add_row(
str(module.get("id")),
f"[bold green]{module.get('name')}[/bold green]",
Expand All @@ -294,6 +296,8 @@ def list_available_module_types(

Console().print(table)

emit(module_types, output, table_fn=_render_table)

except MoodleModuleError as e:
typer.echo(f"Error listing module types: {e}", err=True)
raise typer.Exit(1)
Expand Down
59 changes: 59 additions & 0 deletions src/py_moodle/cli/output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Shared output format utilities for the ``py-moodle`` CLI."""

from __future__ import annotations

import enum
import json
from typing import Any, Callable, Optional

import typer
import yaml


class OutputFormat(str, enum.Enum):
"""Supported CLI output formats.

Attributes:
TABLE: Human-readable rich table (default).
JSON: Machine-readable JSON.
YAML: Machine-readable YAML.
"""

TABLE = "table"
JSON = "json"
YAML = "yaml"


def emit(
data: Any,
output_format: OutputFormat,
table_fn: Optional[Callable[[Any], None]] = None,
) -> None:
"""Emit ``data`` in the requested output format.

Args:
data: The data to emit. For JSON/YAML this must be serializable.
output_format: The desired output format.
table_fn: A callable that renders ``data`` as a rich table.
Required when ``output_format`` is ``OutputFormat.TABLE``.

Raises:
ValueError: If ``output_format`` is ``TABLE`` and no ``table_fn``
is provided.
"""
if output_format == OutputFormat.JSON:
typer.echo(json.dumps(data, indent=2, ensure_ascii=False))
elif output_format == OutputFormat.YAML:
typer.echo(
yaml.dump(data, allow_unicode=True, default_flow_style=False),
nl=False,
)
else:
if table_fn is None:
raise ValueError(
"table_fn is required when output_format is OutputFormat.TABLE"
)
table_fn(data)


__all__ = ["OutputFormat", "emit"]
Loading
Loading