petlja-api 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
petlja_api/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .auth import login
2
+ from .competition import create_competition, get_competition_id, get_added_problem_ids, add_problem, upload_scoring, add_language
3
+ from .problem import get_problem_id, get_problem_name, create_problem, upload_testcases, upload_statement
4
+ from .submit import submit_solution
petlja_api/auth.py ADDED
@@ -0,0 +1,48 @@
1
+ from getpass import getpass
2
+
3
+ import requests
4
+ from bs4 import BeautifulSoup
5
+
6
+ from .urls import ARENA_URL
7
+
8
+
9
+ def _get_credentials():
10
+ username = input("Petlja username: ")
11
+ password = getpass("Petlja password: ")
12
+ return username, password
13
+
14
+
15
+ def get_csrf_token(page):
16
+ soup = BeautifulSoup(page, "html.parser")
17
+ csrf_token = soup.find("input", {"name": "__RequestVerificationToken"}).get("value")
18
+ return csrf_token
19
+
20
+
21
+ def login(username=None, password=None):
22
+ if username is None or password is None:
23
+ username, password = _get_credentials()
24
+
25
+ session = requests.Session()
26
+ mainpage = session.get(ARENA_URL)
27
+ csrf_token = get_csrf_token(mainpage.text)
28
+ login_res = session.post(
29
+ f"{ARENA_URL}/sr-Latn-RS/Account/Login",
30
+ data={
31
+ "LoginViewModel.UserNameOrEmail": username,
32
+ "LoginViewModel.Password": password,
33
+ "__RequestVerificationToken": csrf_token,
34
+ "LoginViewModel.RememberMe": "false",
35
+ },
36
+ allow_redirects=False,
37
+ )
38
+
39
+ if login_res.status_code == 302:
40
+ jwt = session.cookies["PetljaCookie"]
41
+ session.headers.update({"Authorization": f"Bearer {jwt}"})
42
+ return session
43
+ elif login_res.status_code == 200:
44
+ raise PermissionError("Wrong username or password")
45
+ else:
46
+ raise RuntimeError(
47
+ f"Unknown error: login failed with status code {login_res.status_code}"
48
+ )
@@ -0,0 +1,131 @@
1
+ import json
2
+ import re
3
+ from datetime import datetime
4
+
5
+ from bs4 import BeautifulSoup
6
+
7
+ from .auth import get_csrf_token
8
+ from .problem import get_problem_name
9
+ from .urls import ARENA_URL, CPANEL_URL, PETLJA_URL
10
+ from .submit import LANGUAGE_IDS
11
+
12
+
13
+ def get_competition_id(session, alias):
14
+ page = session.get(f"{ARENA_URL}/competition/{alias}")
15
+ if (
16
+ page.status_code == 404
17
+ or 'error' in page.url
18
+ ):
19
+ raise ValueError(f"Competition with alias {alias} does not exist")
20
+
21
+ soup = BeautifulSoup(page.text, "html.parser")
22
+ competition_id = soup.find("button", attrs={"id": "ciRun"})["data-competition-id"]
23
+ return competition_id
24
+
25
+
26
+ def get_added_problem_ids(session, competition_id):
27
+ page = session.get(f"{CPANEL_URL}/CompetitionTasks/{competition_id}")
28
+ soup = BeautifulSoup(page.text, "html.parser")
29
+ # Get object viewModel from inline script in html which contains data about added problems
30
+ # FIXME regex hack, should be replaced with a proper parser
31
+ regex = re.compile(r"var viewModel=({.*?});\n")
32
+ match = regex.search(soup.prettify()).group(1)
33
+ # Can be parsed as json since it is a javascript object
34
+ data = json.loads(match)
35
+ problem_ids = [str(problem["problemId"]) for problem in data["problems"]]
36
+ return problem_ids
37
+
38
+
39
+ def create_competition(
40
+ session, name, alias=None, description=None, start_date=None, end_date=None
41
+ ):
42
+ if alias is None:
43
+ alias = ""
44
+ if description is None:
45
+ description = ""
46
+ if start_date is None:
47
+ start_date = datetime.now()
48
+ if end_date is None:
49
+ end_date = ""
50
+
51
+ regex = re.compile(r"^[a-z0-9-]+$")
52
+ if not regex.match(alias):
53
+ raise NameError(
54
+ f"Invalid alias {alias}: must contain only lowercase alphanumeric characters and dashes"
55
+ )
56
+
57
+ url = f"{CPANEL_URL}/CreateCompetition"
58
+ page = session.get(url)
59
+ csrf_token = get_csrf_token(page.text)
60
+ resp = session.post(
61
+ url,
62
+ data={
63
+ "Name": name,
64
+ "Alias": alias,
65
+ "Description": description,
66
+ "StartDate": start_date,
67
+ "EndDate": end_date,
68
+ "HasNotEndDate": [True, False], # Not sure what this field does
69
+ "__RequestVerificationToken": csrf_token,
70
+ },
71
+ allow_redirects=False,
72
+ )
73
+
74
+ if resp.status_code == 302:
75
+ header_loc = resp.headers["Location"] # /cpanel/CompetitionSettings/:comp_id
76
+ comp_id = header_loc.split("/")[-1]
77
+ return comp_id
78
+ elif resp.status_code == 200:
79
+ raise ValueError("Competition alias already exists")
80
+ else:
81
+ raise RuntimeError(f"Unknown error: {resp.status_code}")
82
+
83
+
84
+ def add_problem(session, competition_id, problem_id):
85
+ already_added = get_added_problem_ids(session, competition_id)
86
+ if problem_id in already_added:
87
+ return
88
+
89
+ url = f"{PETLJA_URL}/api/dashboard/competitions/problems/add"
90
+ problem_name = get_problem_name(session, problem_id)
91
+ resp = session.post(
92
+ url,
93
+ json={
94
+ "competitionId": competition_id,
95
+ "problemId": problem_id,
96
+ "name": problem_name,
97
+ # "sortOrder": 0, # Seems to be optional
98
+ },
99
+ )
100
+
101
+ # TODO: Check for errors
102
+
103
+
104
+ def upload_scoring(session, competition_id, problem_id, scoring_path):
105
+ url = f"{PETLJA_URL}/api/dashboard/competitions/problems/addGraderHints"
106
+ with open(scoring_path) as scoring_file:
107
+ scoring = scoring_file.read()
108
+ resp = session.post(
109
+ url,
110
+ json={
111
+ "competitionId": competition_id,
112
+ "problemId": problem_id,
113
+ "hintsYML": scoring,
114
+ "testCaseCount": 100, # FIXME Count number of testcases from scoring file
115
+ },
116
+ )
117
+
118
+ errors = resp.json()["errors"]
119
+ if errors:
120
+ raise ValueError(f"Error uploading scoring, petlja response: {errors[0]}")
121
+
122
+ def add_language(session, competition_id, extension):
123
+ url = f'{PETLJA_URL}/api/dashboard/competitions/programmingLanguages/add'
124
+ res = session.post(
125
+ url,
126
+ json={
127
+ "competitionId": competition_id,
128
+ "languageId": LANGUAGE_IDS[extension]
129
+ }
130
+ )
131
+ # TODO Check for errors
petlja_api/problem.py ADDED
@@ -0,0 +1,98 @@
1
+ from bs4 import BeautifulSoup
2
+
3
+ from .auth import get_csrf_token
4
+ from .urls import CPANEL_URL, PROBLEMS_URL
5
+
6
+
7
+ def get_problem_id(session, alias):
8
+ page = session.get(f"{PROBLEMS_URL}/{alias}")
9
+ if page.status_code == 404:
10
+ raise ValueError(f"Problem with alias {alias} does not exist")
11
+
12
+ soup = BeautifulSoup(page.text, "html.parser")
13
+ problem_id = soup.find("button", attrs={"class": "btn-solution-submit"})[
14
+ "data-problem-id"
15
+ ]
16
+ return problem_id
17
+
18
+
19
+ def get_problem_name(session, problem_id):
20
+ page = session.get(f"{CPANEL_URL}/EditProblem/{problem_id}")
21
+ if page.status_code == 404:
22
+ raise ValueError(f"Problem with id {problem_id} does not exist")
23
+
24
+ soup = BeautifulSoup(page.text, "html.parser")
25
+ problem_name = soup.find("input", attrs={"id": "Problem_Name"})["value"]
26
+ return problem_name
27
+
28
+
29
+ def create_problem(session, name, alias):
30
+ if not alias or not alias.isalnum() or not alias.islower():
31
+ raise NameError(
32
+ f"Invalid problem alias {alias}: must be alphanumeric and lowercase"
33
+ )
34
+
35
+ create_problem_page = session.get(f"{CPANEL_URL}/CreateTask")
36
+ csrf_token = get_csrf_token(create_problem_page.text)
37
+ resp = session.post(
38
+ f"{CPANEL_URL}/CreateTask",
39
+ data={
40
+ "Name": name,
41
+ "UniqueId": alias,
42
+ "Type": "0",
43
+ "__RequestVerificationToken": csrf_token,
44
+ },
45
+ allow_redirects=False
46
+ )
47
+ if resp.status_code == 302:
48
+ return get_problem_id(session, alias)
49
+ elif resp.status_code == 200:
50
+ raise ValueError("Problem alias already exists")
51
+ else:
52
+ raise RuntimeError(
53
+ "Unknown error while creating problem (status code {resp.status_code})"
54
+ )
55
+
56
+
57
+ def check_testcase_upload(page):
58
+ soup = BeautifulSoup(page.text, "html.parser")
59
+ error = soup.find("div", attrs={"class": "validation-summary-errors"})
60
+ if error:
61
+ errmsg = f"Testcase upload failed: {error.text.strip()}"
62
+ if error.text.strip() == "UserIdNotFound":
63
+ errmsg += " (Is the zip file in the correct format?)"
64
+ raise ValueError(errmsg)
65
+
66
+
67
+ def upload_testcases(session, problem_id, testcases_path):
68
+ page = session.get(f"{CPANEL_URL}/EditProblem/{problem_id}?tab=testcases")
69
+ csrf_token = get_csrf_token(page.text)
70
+ with open(testcases_path, "rb") as zipfile:
71
+ resp = session.post(
72
+ f"{CPANEL_URL}/EditProblem/{problem_id}",
73
+ files={"TestCases": zipfile},
74
+ data={
75
+ "PostAction": "EditTestCases",
76
+ "__RequestVerificationToken": csrf_token,
77
+ },
78
+ )
79
+ # Have to scrape the response page to check for errors
80
+ # because the response is 302 even if there is an error
81
+ check_testcase_upload(resp)
82
+
83
+
84
+ def upload_statement(session, problem_id, statement_path):
85
+ page = session.get(f"{CPANEL_URL}/EditProblem/{problem_id}?tab=statement")
86
+ csrf_token = get_csrf_token(page.text)
87
+ with open(statement_path, encoding="utf-8") as statement:
88
+ resp = session.post(
89
+ f"{CPANEL_URL}/EditProblem/{problem_id}",
90
+ data={
91
+ "Problem.ProblemStatementMD": statement.read(),
92
+ "PostAction": "EditStatement",
93
+ "Problem.MDSupported": "true",
94
+ "__RequestVerificationToken": csrf_token,
95
+ },
96
+ allow_redirects=False,
97
+ )
98
+ # TODO: check for errors
petlja_api/submit.py ADDED
@@ -0,0 +1,54 @@
1
+ from .urls import ARENA_URL
2
+ import time
3
+
4
+ # Weird Petlja API language IDs
5
+ LANGUAGE_IDS = {
6
+ ".c": 10,
7
+ ".cs": 1,
8
+ ".cpp": 2,
9
+ ".java": 3,
10
+ ".m": 7,
11
+ ".pas": 4,
12
+ ".py": 9,
13
+ }
14
+
15
+ TIMEOUT = 30
16
+
17
+
18
+ def submit_solution(session, competition_id, problem_id, source_path):
19
+ with open(source_path) as source_file:
20
+ source = source_file.read()
21
+
22
+ extension = source_path.suffix
23
+ submit_res = session.post(
24
+ f"{ARENA_URL}/api/competition/submit-competition-problem",
25
+ json={
26
+ "competitionId": competition_id,
27
+ "problemId": problem_id,
28
+ "source": source,
29
+ "languageId": LANGUAGE_IDS[extension],
30
+ },
31
+ )
32
+ success = submit_res.json()["succeeded"]
33
+ if not success:
34
+ error = submit_res.json()["errors"][0]["description"]
35
+ raise Exception(error)
36
+ submission_id = submit_res.json()["value"]
37
+ # Polling the server every x seconds
38
+ # Better solution may exist
39
+ tries = 0
40
+ while tries < TIMEOUT:
41
+ submission_data = session.post(
42
+ f"{ARENA_URL}/api/competition/submissions",
43
+ json={
44
+ "competitionId": competition_id,
45
+ "idStamp": submission_id,
46
+ "loadNew": True,
47
+ },
48
+ )
49
+ score = submission_data.json()["value"]["item1"][0]["score"]
50
+ if score != '-':
51
+ return score
52
+ time.sleep(1)
53
+
54
+ raise TimeoutError
petlja_api/urls.py ADDED
@@ -0,0 +1,4 @@
1
+ ARENA_URL = "https://arena.petlja.org"
2
+ PETLJA_URL = "https://petlja.org"
3
+ CPANEL_URL = f"{PETLJA_URL}/sr-Latn-RS/cpanel"
4
+ PROBLEMS_URL = f"{PETLJA_URL}/sr-Latn-RS/problems"
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.3
2
+ Name: petlja_api
3
+ Version: 0.0.1
4
+ Summary: A python library for interacting with the petlja.org API
5
+ Project-URL: Homepage, https://github.com/PavleSekesan/petlja_api
6
+ Author-email: Pavle Sekešan <pavlesekesan@gmail.com>
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.7
9
+ Requires-Dist: beautifulsoup4==4.12.2
10
+ Requires-Dist: requests==2.31.0
11
+ Description-Content-Type: text/markdown
12
+
13
+ # petlja_api
14
+
15
+ A python library for interacting with the [petlja.org](https://petlja.org/) API.
16
+
17
+ ## Installation
18
+ ```
19
+ pip install petlja_api
20
+ ```
21
+
22
+ ## Basic usage
23
+
24
+ ```py
25
+ import petlja_api as petlja
26
+
27
+ session = petlja.login()
28
+
29
+ # Create problem
30
+ prob_id = petlja.create_problem(session, name="My Problem", alias="my-prob")
31
+ petlja.upload_testcases(session, prob_id, "my-prob/testcases.zip")
32
+ petlja.upload_statement(session, prob_id, "my-prob/statement.md")
33
+
34
+ # Create competition
35
+ comp_id = petlja.create_competition(session, name="My Competition", alias="my-comp")
36
+ petlja.add_problem(session, comp_id, prob_id)
37
+
38
+ # Upload solution
39
+ petlja.score = submit(session, prob_id, "my-prob/sol.cpp", comp_id)
40
+ ```
@@ -0,0 +1,10 @@
1
+ petlja_api/__init__.py,sha256=FoR5XbbCMOReJx9SpUv6Meut21WesaeaDSou9Dt0oNk,295
2
+ petlja_api/auth.py,sha256=m6hvN_hq73bpaawokdugI4TChWxWNh9E3Een1m8o0sE,1442
3
+ petlja_api/competition.py,sha256=Obzh90yvaP2AKcj0UDr_WwQ1nwiG60rUEyn3gjdnnEY,4209
4
+ petlja_api/problem.py,sha256=RhIgkCCWG33JKD9mCnYGGsBHEXCUNQKJvvyg2xcU3OQ,3457
5
+ petlja_api/submit.py,sha256=yMY_VdS1pktL47yzUPvp66RA01ezEZOFUatF0Kt_Sqs,1436
6
+ petlja_api/urls.py,sha256=ol2xDoGg8pZS3I_kXrAMfsHX_NI54XtDlCI9qfI3u84,170
7
+ petlja_api-0.0.1.dist-info/METADATA,sha256=V4zxVbggFapS5n7wguNaqOoat-poeg66k630462tb68,1088
8
+ petlja_api-0.0.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
9
+ petlja_api-0.0.1.dist-info/licenses/LICENSE,sha256=vjjmZ_TFg_A8t-6PHeFUaEo1pSrRtnO59PBzc527jS0,1071
10
+ petlja_api-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.25.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Pavle Sekešan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.