Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement submit action #33

Merged
Merged
Show file tree
Hide file tree
Changes from 5 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
17 changes: 5 additions & 12 deletions tests/test_tyora.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ class TestFindLink:
valid_return = {"href": "somelink", "text": "sometext"}

def test_find_link_success(self) -> None:
assert (
tyora.find_link(self.valid_html, self.valid_xpath)
== self.valid_return
)
assert tyora.find_link(self.valid_html, self.valid_xpath) == self.valid_return

def test_find_link_bad_xpath(self) -> None:
assert tyora.find_link(self.valid_html, self.invalid_xpath) == {}
Expand Down Expand Up @@ -84,17 +81,13 @@ def test_login_failed(mock_session: tyora.Session) -> None:


# TODO: functions that use user input or read or write files
def test_create_config() -> None:
...
def test_create_config() -> None: ...


def test_write_config() -> None:
...
def test_write_config() -> None: ...


def test_read_config() -> None:
...
def test_read_config() -> None: ...


def test_get_cookiejar() -> None:
...
def test_get_cookiejar() -> None: ...
108 changes: 93 additions & 15 deletions tyora.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
from urllib.parse import urljoin
from pathlib import Path
from time import sleep
from typing import AnyStr, Optional
from xml.etree.ElementTree import Element, tostring

Expand Down Expand Up @@ -145,6 +146,15 @@ def parse_args(args: Optional[list[str]] = None) -> argparse.Namespace:
parser_show.set_defaults(cmd="show")
parser_show.add_argument("task_id", help="Numerical task identifier")

# submit exercise solution subparser
parser_submit = subparsers.add_parser("submit", help="Submit an exercise solution")
parser_submit.set_defaults(cmd="submit")
parser_submit.add_argument(
"--filename",
help="Filename of the solution to submit (if not given will be guessed from task description)",
)
parser_submit.add_argument("task_id", help="Numerical task identifier")

return parser.parse_args(args)


Expand Down Expand Up @@ -221,7 +231,7 @@ def find_link(html: AnyStr, xpath: str) -> dict[str, str | None]:
return link_data


def parse_form(html: AnyStr, xpath: str = ".//form") -> dict[str, str | None]:
def parse_form(html: AnyStr, xpath: str = ".//form") -> dict:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The modification in the parse_form function to remove type hints for return values aligns with the changes in other parts of the codebase. However, consider adding a more detailed docstring to explain the parameters and the return type more clearly.

def parse_form(html: AnyStr, xpath: str = ".//form") -> dict:
    """
    Search for the first form in html and return a dictionary with action and all other found inputs.

    Args:
        html (AnyStr): HTML content as a string.
        xpath (str): XPath query to locate the form.

    Returns:
        dict: Dictionary containing form action and inputs.
    """
    # function body remains the same

"""Search for the first form in html and return dict with action and all other found inputs"""
form_element = htmlement.fromstring(html).find(xpath)
form_data = dict()
Expand Down Expand Up @@ -251,9 +261,10 @@ class Task:
id: str
name: str
state: TaskState
description: str = "N/A"
code: str = "N/A"
submit_file: str = "N/A"
description: Optional[str] = None
code: Optional[str] = None
submit_file: Optional[str] = None
submit_link: Optional[str] = None


def parse_task_list(html: AnyStr) -> list[Task]:
Expand All @@ -279,9 +290,11 @@ def parse_task_list(html: AnyStr) -> list[Task]:
task = Task(
id=item_id,
name=item_name,
state=TaskState.COMPLETE
if "full" in item_class
else TaskState.INCOMPLETE,
state=(
TaskState.COMPLETE
if "full" in item_class
else TaskState.INCOMPLETE
),
)
task_list.append(task)

Expand All @@ -305,14 +318,25 @@ def parse_task(html: AnyStr) -> Task:
task_link_element = root.find('.//div[@class="nav sidebar"]/a')
task_link = task_link_element if task_link_element is not None else Element("a")
task_id = task_link.get("href", "").split("/")[-1]
task_name = task_link.text or "N/A"
if not task_id:
raise ValueError("Failed to find task id")
task_name = task_link.text or None
if not task_name:
raise ValueError("Failed to find task name")
task_span_element = task_link.find("span")
task_span = task_span_element if task_span_element is not None else Element("span")
task_span_class = task_span.get("class", "")
desc_div_element = root.find('.//div[@class="md"]')
desc_div = desc_div_element if desc_div_element is not None else Element("div")
description = html2text(tostring(desc_div).decode("utf8"))
code = root.findtext(".//pre", "N/A")
code = root.findtext(".//pre", None)
submit_link_element = root.find('.//a[.="Submit"]')
submit_link = (
submit_link_element.get("href", None)
if submit_link_element is not None
else None
)

submit_file = next(
iter(
[
Expand All @@ -321,7 +345,7 @@ def parse_task(html: AnyStr) -> Task:
if code_element.text is not None and ".py" in code_element.text
]
),
"N/A",
None,
)
task = Task(
id=task_id,
Expand All @@ -330,6 +354,7 @@ def parse_task(html: AnyStr) -> Task:
description=description.strip(),
code=code,
submit_file=submit_file,
submit_link=submit_link,
)

return task
Expand All @@ -341,10 +366,13 @@ def print_task(task: Task) -> None:
print(f"\nSubmission file name: {task.submit_file}")


def submit_task(task_id: str, filename: str) -> None:
"""submit file to the submit form or task_id"""
# NOTE: use parse_form
...
# def submit_task(task_id: str, filename: str) -> None:
# """submit file to the submit form or task_id"""
# html = session.http_request(urljoin(base_url, f"task/{task_id}"))
# task = parse_task(html)
# answer = input("Do you want to submit this task? (y/n): ")
# if answer in ('y', 'Y'):
# with open(filename, 'r') as f:


def main() -> None:
Expand Down Expand Up @@ -396,9 +424,59 @@ def main() -> None:

if args.cmd == "show":
html = session.http_request(urljoin(base_url, f"task/{args.task_id}"))
task = parse_task(html)
try:
task = parse_task(html)
except ValueError as e:
logger.debug(f"Error parsing task: {e}")
raise
print_task(task)

if args.cmd == "submit":
html = session.http_request(urljoin(base_url, f"task/{args.task_id}"))
task = parse_task(html)
if not task.submit_file and not args.filename:
raise ValueError("No submission filename found")
if not task.submit_link:
raise ValueError("No submission link found")
submit_file = args.filename or task.submit_file or ""

submit_form_html = session.http_request(urljoin(base_url, task.submit_link))
submit_form_data = parse_form(submit_form_html)
action = submit_form_data.pop("_action")

for key, value in submit_form_data.items():
submit_form_data[key] = (None, value)
submit_form_data["file"] = (submit_file, open(submit_file, "rb"))
submit_form_data["lang"] = (None, "Python3")
submit_form_data["option"] = (None, "CPython3")

res = session.http_session.post(
urljoin(base_url, action), files=submit_form_data
)
html = res.text
result_url = res.url
print("Waiting for test results.", end="")
while "Test report" not in html:
print(".", end="")
sleep(1)
html = session.http_request(result_url)

print()
root = htmlement.fromstring(html)
submit_status_element = root.find('.//td[.="Status:"]/..') or Element("td")
submit_status_span_element = submit_status_element.find("td/span") or Element(
"span"
)
submit_status = submit_status_span_element.text or ""
submit_result_element = root.find('.//td[.="Result:"]/..') or Element("td")
submit_result_span_element = submit_result_element.find("td/span") or Element(
"span"
)
submit_result = submit_result_span_element.text or ""

print(f"Submission status: {submit_status.lower()}")
print(f"Submission result: {submit_result.lower()}")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The submission logic integrated into the main function is complex and involves multiple steps, including form parsing, file submission, and status checking. Consider refactoring this into a separate function or class to improve maintainability and testability.

def submit_task(session: Session, task: Task, filename: str) -> None:
    """
    Handles the submission of a task using the provided session and task details.

    Args:
        session (Session): The session object with login details and cookies.
        task (Task): The task object containing submission details.
        filename (str): The filename of the solution to submit.
    """
    # Move the submission logic here


if __name__ == "__main__":
main()