petlja-api 0.0.1__tar.gz
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-0.0.1/.gitignore +160 -0
- petlja_api-0.0.1/.vscode/settings.json +11 -0
- petlja_api-0.0.1/LICENSE +21 -0
- petlja_api-0.0.1/PKG-INFO +40 -0
- petlja_api-0.0.1/README.md +28 -0
- petlja_api-0.0.1/examples/petljatest/README.md +55 -0
- petlja_api-0.0.1/examples/petljatest/petljatest.py +96 -0
- petlja_api-0.0.1/pyproject.toml +20 -0
- petlja_api-0.0.1/src/petlja_api/__init__.py +4 -0
- petlja_api-0.0.1/src/petlja_api/auth.py +48 -0
- petlja_api-0.0.1/src/petlja_api/competition.py +131 -0
- petlja_api-0.0.1/src/petlja_api/problem.py +98 -0
- petlja_api-0.0.1/src/petlja_api/submit.py +54 -0
- petlja_api-0.0.1/src/petlja_api/urls.py +4 -0
- petlja_api-0.0.1/tests/data/testcases.zip +0 -0
- petlja_api-0.0.1/tests/test_modules.py +236 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# poetry
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
102
|
+
#poetry.lock
|
|
103
|
+
|
|
104
|
+
# pdm
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
106
|
+
#pdm.lock
|
|
107
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
108
|
+
# in version control.
|
|
109
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
110
|
+
.pdm.toml
|
|
111
|
+
|
|
112
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
113
|
+
__pypackages__/
|
|
114
|
+
|
|
115
|
+
# Celery stuff
|
|
116
|
+
celerybeat-schedule
|
|
117
|
+
celerybeat.pid
|
|
118
|
+
|
|
119
|
+
# SageMath parsed files
|
|
120
|
+
*.sage.py
|
|
121
|
+
|
|
122
|
+
# Environments
|
|
123
|
+
.env
|
|
124
|
+
.venv
|
|
125
|
+
env/
|
|
126
|
+
venv/
|
|
127
|
+
ENV/
|
|
128
|
+
env.bak/
|
|
129
|
+
venv.bak/
|
|
130
|
+
|
|
131
|
+
# Spyder project settings
|
|
132
|
+
.spyderproject
|
|
133
|
+
.spyproject
|
|
134
|
+
|
|
135
|
+
# Rope project settings
|
|
136
|
+
.ropeproject
|
|
137
|
+
|
|
138
|
+
# mkdocs documentation
|
|
139
|
+
/site
|
|
140
|
+
|
|
141
|
+
# mypy
|
|
142
|
+
.mypy_cache/
|
|
143
|
+
.dmypy.json
|
|
144
|
+
dmypy.json
|
|
145
|
+
|
|
146
|
+
# Pyre type checker
|
|
147
|
+
.pyre/
|
|
148
|
+
|
|
149
|
+
# pytype static type analyzer
|
|
150
|
+
.pytype/
|
|
151
|
+
|
|
152
|
+
# Cython debug symbols
|
|
153
|
+
cython_debug/
|
|
154
|
+
|
|
155
|
+
# PyCharm
|
|
156
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
157
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
158
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
159
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
160
|
+
#.idea/
|
petlja_api-0.0.1/LICENSE
ADDED
|
@@ -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.
|
|
@@ -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,28 @@
|
|
|
1
|
+
# petlja_api
|
|
2
|
+
|
|
3
|
+
A python library for interacting with the [petlja.org](https://petlja.org/) API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
```
|
|
7
|
+
pip install petlja_api
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Basic usage
|
|
11
|
+
|
|
12
|
+
```py
|
|
13
|
+
import petlja_api as petlja
|
|
14
|
+
|
|
15
|
+
session = petlja.login()
|
|
16
|
+
|
|
17
|
+
# Create problem
|
|
18
|
+
prob_id = petlja.create_problem(session, name="My Problem", alias="my-prob")
|
|
19
|
+
petlja.upload_testcases(session, prob_id, "my-prob/testcases.zip")
|
|
20
|
+
petlja.upload_statement(session, prob_id, "my-prob/statement.md")
|
|
21
|
+
|
|
22
|
+
# Create competition
|
|
23
|
+
comp_id = petlja.create_competition(session, name="My Competition", alias="my-comp")
|
|
24
|
+
petlja.add_problem(session, comp_id, prob_id)
|
|
25
|
+
|
|
26
|
+
# Upload solution
|
|
27
|
+
petlja.score = submit(session, prob_id, "my-prob/sol.cpp", comp_id)
|
|
28
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# petljatest
|
|
2
|
+
|
|
3
|
+
Automatically create a competition, add its problems and test all of their solutions on [arena.petlja.org](https://arena.petlja.org/)
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
Directory structure should be [petljapub](https://pypi.org/project/petljapub/) compatible
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
00_competition/
|
|
11
|
+
├── 01_problem1
|
|
12
|
+
│ ├── problem1.cpp
|
|
13
|
+
│ ├── problem1.py
|
|
14
|
+
│ ├── problem1-sol.md
|
|
15
|
+
│ ├── problem1-st.md
|
|
16
|
+
│ ├── problem1-tgen.cpp
|
|
17
|
+
│ └── scoring.yaml
|
|
18
|
+
├── 02_problem2
|
|
19
|
+
│ ├── problem2.cpp
|
|
20
|
+
│ ├── problem2.cs
|
|
21
|
+
│ ├── problem2.py
|
|
22
|
+
│ ├── problem2-scoring.yaml
|
|
23
|
+
│ ├── problem2-sol.md
|
|
24
|
+
│ ├── problem2-st.md
|
|
25
|
+
│ └── problem2-tgen.cpp
|
|
26
|
+
├── ...
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Run script from top-level competition directory
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
$ cd 00_competition/
|
|
33
|
+
$ python3 petljatest.py
|
|
34
|
+
Petlja username: testacc
|
|
35
|
+
Petlja password:
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Example output:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
Created competition 00_example_competition
|
|
42
|
+
Found problem 01_problem1
|
|
43
|
+
Uploaded testcases for problem 01_problem1
|
|
44
|
+
Uploaded statement for problem 01_problem1
|
|
45
|
+
Added problem 01_problem1 to competition 00_example_competition
|
|
46
|
+
Submitted solution problem1.py for problem 01_problem1
|
|
47
|
+
Score: 10
|
|
48
|
+
Submitted solution problem1.cs for problem 01_problem1
|
|
49
|
+
Score: 0
|
|
50
|
+
Submitted solution problem1.cpp for problem 01_problem1
|
|
51
|
+
Score: 0
|
|
52
|
+
Found problem 02_problem2
|
|
53
|
+
...
|
|
54
|
+
```
|
|
55
|
+
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import petlja_api as petlja
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def is_for_testing(source_path):
|
|
8
|
+
is_code = source_path.suffix in petlja.submit.LANGUAGE_IDS
|
|
9
|
+
is_tgen = source_path.stem.endswith("tgen")
|
|
10
|
+
return is_code and not is_tgen
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def remove_metadata(st_path):
|
|
14
|
+
cleaned = Path(st_path.parent, st_path.stem + "-cleaned.md")
|
|
15
|
+
is_metadata = False
|
|
16
|
+
lines = open(st_path, encoding="utf-8").readlines()
|
|
17
|
+
with open(cleaned, "w", encoding="utf-8") as cleaned_file:
|
|
18
|
+
for line in lines:
|
|
19
|
+
if line.startswith("---"):
|
|
20
|
+
is_metadata = not is_metadata
|
|
21
|
+
continue
|
|
22
|
+
if not is_metadata:
|
|
23
|
+
cleaned_file.write(line)
|
|
24
|
+
return cleaned
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def alias_from_name(name):
|
|
28
|
+
alias = re.sub(r"[^a-zA-Z0-9]", "", name).lower()
|
|
29
|
+
return alias
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def open_comp(comp_name):
|
|
33
|
+
comp_alias = alias_from_name(comp_name)
|
|
34
|
+
try:
|
|
35
|
+
comp_id = petlja.create_competition(session, comp_name, comp_alias)
|
|
36
|
+
print(f"Created competition {comp_name}")
|
|
37
|
+
except ValueError:
|
|
38
|
+
comp_id = petlja.get_competition_id(session, comp_alias)
|
|
39
|
+
print(f"Found competition {comp_name}")
|
|
40
|
+
return comp_id
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def open_prob(prob_name):
|
|
44
|
+
prob_alias = alias_from_name(prob_name)
|
|
45
|
+
try:
|
|
46
|
+
prob_id = petlja.create_problem(session, prob_name, prob_alias)
|
|
47
|
+
print(f"Created problem {prob_name}")
|
|
48
|
+
except ValueError:
|
|
49
|
+
prob_id = petlja.get_problem_id(session, prob_alias)
|
|
50
|
+
print(f"Found problem {prob_name}")
|
|
51
|
+
return prob_id
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
session = petlja.login()
|
|
55
|
+
|
|
56
|
+
comp_path = Path.cwd()
|
|
57
|
+
comp_name = comp_path.name
|
|
58
|
+
comp_id = open_comp(comp_name)
|
|
59
|
+
|
|
60
|
+
for problem_path in comp_path.iterdir():
|
|
61
|
+
if problem_path.is_dir():
|
|
62
|
+
problem_name = problem_path.name
|
|
63
|
+
problem_id = open_prob(problem_name)
|
|
64
|
+
|
|
65
|
+
subprocess.run(
|
|
66
|
+
["petljapub", "tests-zip"], cwd=problem_path,
|
|
67
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
|
68
|
+
)
|
|
69
|
+
testcases_path = problem_path.joinpath("_build/testcases.zip")
|
|
70
|
+
petlja.upload_testcases(session, problem_id, testcases_path)
|
|
71
|
+
print(f"Uploaded testcases for problem {problem_name}")
|
|
72
|
+
|
|
73
|
+
statement_path = next(problem_path.glob("*-st.md"))
|
|
74
|
+
cleaned_statement_path = remove_metadata(statement_path)
|
|
75
|
+
petlja.upload_statement(session, problem_id, cleaned_statement_path)
|
|
76
|
+
cleaned_statement_path.unlink()
|
|
77
|
+
print(f"Uploaded statement for problem {problem_name}")
|
|
78
|
+
|
|
79
|
+
petlja.add_problem(session, comp_id, problem_id)
|
|
80
|
+
print(f"Added problem {problem_name} to competition {comp_name}")
|
|
81
|
+
|
|
82
|
+
for solution_path in problem_path.iterdir():
|
|
83
|
+
if not is_for_testing(solution_path):
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
solution_name = solution_path.name
|
|
87
|
+
language = solution_path.suffix
|
|
88
|
+
petlja.add_language(session, comp_id, language)
|
|
89
|
+
score = petlja.submit_solution(
|
|
90
|
+
session,
|
|
91
|
+
comp_id,
|
|
92
|
+
problem_id,
|
|
93
|
+
solution_path,
|
|
94
|
+
)
|
|
95
|
+
print(f"Submitted solution {solution_name} for problem {problem_name}")
|
|
96
|
+
print(f"Score: {score}")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "petlja_api"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Pavle Sekešan", email="pavlesekesan@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "A python library for interacting with the petlja.org API"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.7"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"beautifulsoup4==4.12.2",
|
|
16
|
+
"requests==2.31.0"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
"Homepage" = "https://github.com/PavleSekesan/petlja_api"
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
Binary file
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import random
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
import requests
|
|
6
|
+
import petlja_api as petlja
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
load_dotenv()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def sess():
|
|
14
|
+
return petlja.login(os.environ['PETLJA_USER'], os.environ['PETLJA_PASS'])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def created_prob(sess):
|
|
19
|
+
uid = random.randint(1000, 9999)
|
|
20
|
+
alias = f"testprob{uid}"
|
|
21
|
+
pid = petlja.create_problem(sess, "Test zadatak", alias)
|
|
22
|
+
return pid, alias
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def empty_comp(sess):
|
|
27
|
+
uid = random.randint(1000, 9999)
|
|
28
|
+
alias = f"testcomp{uid}"
|
|
29
|
+
cid = petlja.create_competition(sess, "Test takmicenje", alias)
|
|
30
|
+
return cid, alias
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def comp_with_problems(sess, empty_comp, created_prob, scoring, testcases):
|
|
35
|
+
cid, alias = empty_comp
|
|
36
|
+
pid, _ = created_prob
|
|
37
|
+
petlja.add_problem(sess, cid, pid)
|
|
38
|
+
petlja.upload_testcases(sess, pid, testcases)
|
|
39
|
+
petlja.upload_scoring(sess, cid, pid, scoring)
|
|
40
|
+
return cid, alias
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture
|
|
44
|
+
def src_ok(tmp_path):
|
|
45
|
+
src = """
|
|
46
|
+
#include <iostream>
|
|
47
|
+
using namespace std;
|
|
48
|
+
|
|
49
|
+
int main()
|
|
50
|
+
{
|
|
51
|
+
int a, b; cin >> a >> b;
|
|
52
|
+
cout << 2 * (a + b) << endl;
|
|
53
|
+
}
|
|
54
|
+
"""
|
|
55
|
+
path = tmp_path / "trening_ok.cpp"
|
|
56
|
+
path.write_text(src)
|
|
57
|
+
return path
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.fixture
|
|
61
|
+
def src_wa(tmp_path):
|
|
62
|
+
src = """
|
|
63
|
+
#include <iostream>
|
|
64
|
+
using namespace std;
|
|
65
|
+
|
|
66
|
+
int main()
|
|
67
|
+
{
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
"""
|
|
71
|
+
path = tmp_path / "trening_wa.cpp"
|
|
72
|
+
path.write_text(src)
|
|
73
|
+
return path
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.fixture
|
|
77
|
+
def src_py(tmp_path):
|
|
78
|
+
src = """
|
|
79
|
+
a = int(input())
|
|
80
|
+
b = int(input())
|
|
81
|
+
print(2 * (a + b))
|
|
82
|
+
"""
|
|
83
|
+
path = tmp_path / "trening.py"
|
|
84
|
+
path.write_text(src)
|
|
85
|
+
return path
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.fixture
|
|
89
|
+
def statement(tmp_path):
|
|
90
|
+
st = """
|
|
91
|
+
За низ ћемо рећи да је **уравнотежен** ако је збир његових елемената једнак његовој дужини (броју елемената).
|
|
92
|
+
|
|
93
|
+
Дат је низ $a$ дужине $n$, чији су елементи једноцифрени бројеви. Одредити колико он садржи уравнотежених сегмената (поднизова са узастопним елементима).
|
|
94
|
+
|
|
95
|
+
## Улаз
|
|
96
|
+
|
|
97
|
+
У првом реду стандардног улаза је број $n$ $(1 \leq n \leq 10^5)$, а у другом $n$ ненегативних једноцифрених бројева, раздвојених по једним размаком.
|
|
98
|
+
|
|
99
|
+
## Излаз
|
|
100
|
+
|
|
101
|
+
На стандардни излаз исписати тражени број.
|
|
102
|
+
|
|
103
|
+
## Пример
|
|
104
|
+
|
|
105
|
+
### Улаз
|
|
106
|
+
|
|
107
|
+
~~~
|
|
108
|
+
5
|
|
109
|
+
0 3 0 0 2
|
|
110
|
+
~~~
|
|
111
|
+
|
|
112
|
+
### Излаз
|
|
113
|
+
|
|
114
|
+
~~~
|
|
115
|
+
4
|
|
116
|
+
~~~
|
|
117
|
+
|
|
118
|
+
*Објашњење*: Тражени сегменти су $[0 3 0]$, $[3 0 0]$, $[0 2]$ и $[0 3 0 0 2]$.
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
path = tmp_path / "tekst-st.md"
|
|
122
|
+
path.write_text(st)
|
|
123
|
+
return path
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@pytest.fixture
|
|
127
|
+
def testcases():
|
|
128
|
+
return "tests/data/testcases.zip"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@pytest.fixture
|
|
132
|
+
def scoring(tmp_path):
|
|
133
|
+
yaml = """
|
|
134
|
+
type: testcase
|
|
135
|
+
score_total: 100
|
|
136
|
+
score_overrides:
|
|
137
|
+
- {name: 1, score: 10}
|
|
138
|
+
- {name: 2, score: 10}
|
|
139
|
+
- {name: 3, score: 10}
|
|
140
|
+
- {name: 4, score: 10}
|
|
141
|
+
- {name: 5, score: 10}
|
|
142
|
+
- {name: 6, score: 10}
|
|
143
|
+
- {name: 7, score: 10}
|
|
144
|
+
- {name: 8, score: 10}
|
|
145
|
+
- {name: 9, score: 10}
|
|
146
|
+
- {name: 10, score: 10}
|
|
147
|
+
public: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
148
|
+
"""
|
|
149
|
+
scoring_path = tmp_path / "scoring.yaml"
|
|
150
|
+
scoring_path.write_text(yaml)
|
|
151
|
+
return scoring_path
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_login_success(sess):
|
|
155
|
+
assert sess is not None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_login_failed():
|
|
159
|
+
with pytest.raises(PermissionError):
|
|
160
|
+
petlja.login("wrongusername", "wrongpass")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_submit_ok(sess, comp_with_problems, src_ok):
|
|
164
|
+
cid, _ = comp_with_problems
|
|
165
|
+
pid = petlja.get_added_problem_ids(sess, cid)[0]
|
|
166
|
+
score = petlja.submit_solution(sess, cid, pid, src_ok)
|
|
167
|
+
assert int(score) > 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_submit_wa(sess, comp_with_problems, src_wa):
|
|
171
|
+
cid, _ = comp_with_problems
|
|
172
|
+
pid = petlja.get_added_problem_ids(sess, cid)[0]
|
|
173
|
+
score = petlja.submit_solution(sess, cid, pid, src_wa)
|
|
174
|
+
assert score == "0"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_create_problem(created_prob):
|
|
178
|
+
_, alias = created_prob
|
|
179
|
+
res = requests.get(f"https://petlja.org/problems/{alias}")
|
|
180
|
+
assert res.status_code == 200
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_create_already_existing_prob(sess):
|
|
184
|
+
with pytest.raises(ValueError):
|
|
185
|
+
petlja.create_problem(sess, "Postojeci problem", "osdrz23odbijanje")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_upload_testcases(sess, created_prob, testcases):
|
|
189
|
+
id, _ = created_prob
|
|
190
|
+
petlja.upload_testcases(sess, id, testcases)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_upload_statement(sess, created_prob, statement):
|
|
194
|
+
id, _ = created_prob
|
|
195
|
+
petlja.upload_statement(sess, id, statement)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_upload_scoring(sess, comp_with_problems, scoring):
|
|
199
|
+
cid, _ = comp_with_problems
|
|
200
|
+
pid = petlja.get_added_problem_ids(sess, cid)[0]
|
|
201
|
+
petlja.upload_scoring(sess, cid, pid, scoring)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_get_competition_id(sess, empty_comp):
|
|
205
|
+
cid, alias = empty_comp
|
|
206
|
+
assert petlja.get_competition_id(sess, alias) == cid
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_get_competition_id_nonexistent(sess):
|
|
210
|
+
with pytest.raises(ValueError):
|
|
211
|
+
petlja.get_competition_id(sess, 'qurvoqireouqh')
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_submit_unallowed_lang(sess, comp_with_problems, created_prob, src_py):
|
|
215
|
+
cid, _ = comp_with_problems
|
|
216
|
+
pid, _ = created_prob
|
|
217
|
+
with pytest.raises(Exception):
|
|
218
|
+
petlja.submit_solution(sess, cid, pid, src_py)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# Testing TLE is slow
|
|
222
|
+
|
|
223
|
+
# def test_submit_tle(sess, cid, pid, tmp_path):
|
|
224
|
+
# src = """
|
|
225
|
+
# #include <iostream>
|
|
226
|
+
# using namespace std;
|
|
227
|
+
|
|
228
|
+
# int main()
|
|
229
|
+
# {
|
|
230
|
+
# int a, b; cin >> a >> b;
|
|
231
|
+
# while(true) {}
|
|
232
|
+
# }
|
|
233
|
+
# """
|
|
234
|
+
# src_path = tmp_path / "trening_tle.cpp"
|
|
235
|
+
# score = _submit_src_file(sess, cid, pid, src, src_path)
|
|
236
|
+
# assert score == "0"
|