Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.venv
/seen.txt
41 changes: 16 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,44 @@
# Kattis Grind
A small easy-to-use package of Python 3 scripts which can help speed up the process for competitive programming on [Kattis](https://open.kattis.com). The scripts will download a local copy of the question (html file), and can automate testing inputs / outputs.
A small easy-to-use package of Python 3 scripts which can help speed up the process for competitive programming on [Kattis](https://open.kattis.com). The scripts will download a local copy of the question (html file), and can automate testing inputs and outputs.

# Requirements
Kattis Grind uses Python 3. To check if you have it:
```console
foo@bar:~$ python3 --version
Python 3.7.3
```

If you don't have Python 3, you can install it from the [Python Website](https://www.python.org/downloads/).

The Kattis Grind setup requires two modules. To install them:
The Kattis Grind setup requires requests, beautifulsoup4, and fake-useragent. To install them, execute
```console
foo@bar:~$ pip3 install bs4
foo@bar:~$ pip3 install fake-useragent
foo@bar:~$ pip3 install -r requirements.txt
```

# How do I use it?
## Fetch a question!
Simple! If you wanted to fetch a question, you can simply run
## Fetch a Question!
```console
foo@bar:~/kattis-grind$ ./fetch.py
Enter ID:
```
If you prefer to use command line arguments instead, you can! For example, if I wanted to get question 'hello':
```console
foo@bar:~/kattis-grind$ ./fetch.py --id hello
foo@bar:~/kattis-grind$ ./fetch.py hello
```

## Test your solution!
The useful feature for Kattis Grind is that you can easily test your solutions! To do so:
## Test Your Solution!
```console
foo@bar:~/kattis-grind$ ./test.py
Enter ID:
foo@bar:~/kattis-grind$ ./test.py hello
```
Similarly to fetching a question, you can pass in an optional ID argument! Using 'hello' as an example again:
```console
foo@bar:~/kattis-grind$ ./test.py --id hello
```
## Generating random questions!
The best feature of this program is that you can fetch random questions from Kattis too! If I was interested in five questions between a range of 1.4 to 1.6, I can run the script like this:

## Fetch Random Questions!
```console
foo@bar:~/kattis-grind$ ./rand.py
How many problems: 5
Enter lower bound: 1.4
Enter upper bound: 1.6
How many questions: 5
```
It's as simple as that! But as you may have guessed, there are optional command line arguments for these too! Although they're less useful, you can use them like this:

There are optional command line arguments
```console
foo@bar:~/kattis-grind$ ./rand.py --lobound 1.4 --upbound 1.6 --qamount 5
foo@bar:~/kattis-grind$ ./rand.py -n 5 -l 1.4 -u 1.6
```

Depending on how many questions you want, and what range they're between, this operation *may* take several seconds (sometimes a good 10 - 40 seconds!!). Keep in mind that while it takes long, it definitely works!

# Current Bugs
Expand Down
68 changes: 28 additions & 40 deletions fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,77 +5,63 @@
import pathlib
import requests
import argparse

try:
import requests
except ImportError:
sys.exit("You need requests. run 'pip install requests'")

try:
from bs4 import BeautifulSoup
except ImportError:
sys.exit("""You need BeautifulSoup. run \'pip install bs4\'""")
sys.exit("You need BeautifulSoup. run 'pip install beautifulsoup4'")

try:
from fake_useragent import UserAgent
except ImportError:
sys.exit("""You need UserAgent. run \'pip install fake-useragent\'""")
sys.exit("You need UserAgent. run 'pip install fake-useragent'")

parser = argparse.ArgumentParser(description='Fetches random Kattis Questions')
parser.add_argument('--id', type=str, default='_NONE_', help='id of problem to fetch')
parser.add_argument('--hint', type=bool, default=False, help='includes hint from Steve Halim.')
parser = argparse.ArgumentParser(description='Fetches random Kattis Problems')
parser.add_argument('qid', metavar='ID', type=str, help='id of problem to fetch')
args = parser.parse_args()
qid = args.id
hint = args.hint
qid = args.qid

if qid == '_NONE_':
if qid == None:
qid = input('Enter ID: ')
if os.path.isdir('./' + qid):

PROBLEMS_PATH = './problems/'
PROBLEM_PATH = PROBLEMS_PATH + qid

if os.path.isdir(PROBLEM_PATH + qid):
print('This problem already exists.')
exit(1)
pathlib.Path(PROBLEM_PATH).mkdir(parents=True, exist_ok=True)

url = 'http://open.kattis.com/problems/' + qid
sth = 'https://cpbook.net/methodstosolve?oj=kattis&topic=all&quality=all'
usa = UserAgent()

page = requests.get(url, headers={'User-Agent':str(usa.random)})
soup = BeautifulSoup(page.content, 'html.parser')


hinttype = ""
hinttext = ""
if hint:
hintpage = requests.get(sth, headers={'User-Agent': str(usa.random)})
pars = BeautifulSoup(hintpage.content, 'html.parser')
hintinp = pars.find_all('tr', attrs={'class': ['Kattis starred','Kattis nonstarred']})
for hinter in hintinp:
td = hinter.find_all('td')
if td[0].text == qid:
hinttype = td[2].text
hinttext = td[3].text
break

tableinp = soup.find_all('table', attrs={'class': 'sample'})
pathlib.Path(qid).mkdir(parents=True, exist_ok=True)

htmlfile = soup
for section in htmlfile.find_all('section', {'class': 'box clearfix main-content problem-sidebar'}): section.decompose()
for div in htmlfile.find_all('div', {'class':['wrap', 'description','footer','problem-download','footer-powered col-md-8']}): div.decompose()
for img in htmlfile.find_all('img'): img.decompose()
for a in htmlfile.find_all('a'): a.decompose()

if hint:
hinttype_p = htmlfile.new_tag('p')
hinttype_p.string = 'Type: %s'%hinttype
hinttext_p = htmlfile.new_tag('p')
hinttext_p.string = 'Hint: %s'%hinttext
htmlfile.html.append(hinttype_p)
htmlfile.html.append(hinttext_p)

pathlib.Path(qid + '/' + qid + '.html').write_text(str(htmlfile))
pathlib.Path(PROBLEM_PATH + '/' + qid + '.html').write_text(str(htmlfile))

i = 0
for sample in tableinp:
tablestd = sample.find_all('pre')
pathlib.Path(qid + '/input' + str(i + 1)).write_text(tablestd[0].text)
pathlib.Path(qid + '/output' + str(i + 1)).write_text(tablestd[1].text)
pathlib.Path(PROBLEM_PATH + '/input' + str(i + 1)).write_text(tablestd[0].text)
pathlib.Path(PROBLEM_PATH + '/output' + str(i + 1)).write_text(tablestd[1].text)
i += 1

src = os.curdir
dst = os.path.join(src, qid)
dst = os.path.join(src, PROBLEM_PATH)
cpp = os.path.join(src, 'template.cpp')

shutil.copy(cpp, dst)
Expand All @@ -84,9 +70,11 @@
newfile = os.path.join(dst,'_' + qid + '.cpp')
os.rename(dstfile, newfile)

with open('/' + os.getcwd() + '/' + qid + '/_' + qid + '.py', 'w+') as py:
py.write('#!/usr/bin/env python3\n')
os.chmod('/' + os.getcwd() + '/' + qid + '/_' + qid + '.py', 0o777)
python_file_path = (PROBLEM_PATH + '/_' + qid + '.py')

with open(python_file_path, 'w+') as python_file:
python_file.write('#!/usr/bin/env python3\n')
os.chmod(python_file_path, 0o777)

if not os.path.isfile('./seen.txt'):
open('seen.txt', 'w+')
Expand Down
104 changes: 62 additions & 42 deletions rand.py
Original file line number Diff line number Diff line change
@@ -1,82 +1,102 @@
#!/usr/bin/env python3
import os
import sys
import shutil
import random
import pathlib
import requests
import argparse
import subprocess


def get_problem_difficulty(problem):
tds = problem.find_all('td')
text = tds[6].text.split('.')
return float(text[0] + '.' + text[1][:1])


try:
import requests
except ImportError:
sys.exit("You need requests. run 'pip install requests'")

try:
from bs4 import BeautifulSoup
except ImportError:
sys.exit("""You need BeautifulSoup. run \'pip install bs4\'""")
sys.exit("You need BeautifulSoup. run 'pip install bs4'")

try:
from fake_useragent import UserAgent
except ImportError:
sys.exit("""You need UserAgent. run \'pip install fake-useragent\'""")
sys.exit("You need UserAgent. run 'pip install fake-useragent'")

parser = argparse.ArgumentParser(description='Runs Kattis problem through their test cases')
parser.add_argument('--lobound', type=float, default=None, help='the lower bound for questions')
parser.add_argument('--upbound', type=float, default=None, help='the upper bound for questions')
parser.add_argument('--qamount', type=int, default=None, help='the amount of questions wanted to fetch')
parser.add_argument('-n', type=int, default=None, help='the amount of problems wanted to fetch')
parser.add_argument('-l', type=float, default=None, help='the lower bound for problems')
parser.add_argument('-u', type=float, default=None, help='the upper bound for problems')

args = parser.parse_args()
lobound = args.lobound
upbound = args.upbound
n = args.qamount
n= args.n
lower_bound = args.l
upper_bound = args.u

if lobound is None:
lobound = float(input('Enter lower bound: '))
if n is None:
n = int(input('How many problems: '))

if upbound is None:
upbound = float(input('Enter upper bound: '))
if lower_bound is None:
lower_bound = float(input('Enter lower bound: '))

if lobound > 10 or upbound > 10:
exit(0)
if upper_bound is None:
upper_bound = float(input('Enter upper bound: '))

if n is None:
n = int(input('How many questions: '))
tmp = upper_bound
upper_bound = min(max(upper_bound, lower_bound), 10)
lower_bound = max(min(lower_bound, tmp), 0)

seenlist = []
if os.path.isfile('./seen.txt'):
seenlist = [line.rstrip('\n') for line in open('/' + os.getcwd() + '/seen.txt')]
with open('./seen.txt') as f:
seenlist = [line.rstrip('\n') for line in f]

qlist = []
end = False
done = False
i = 0

while end is False:
while not done:
url = 'https://open.kattis.com/problems?page=' + str(i) + '&order=problem_difficulty'
usa = UserAgent()
page = requests.get(url, headers={'User-Agent':str(usa.random)})
soup = BeautifulSoup(page.content, 'html.parser')

table = soup.find('table', attrs={'class': 'problem_list table sortable table-responsive table-kattis center table-hover table-multiple-head-rows table-compact'})
table = soup.find('table', attrs={'class': 'table2'})

if table is None:
end = True
break
done = True

tbody = table.find('tbody')
questions = tbody.find_all('tr')
problems = tbody.find_all('tr')

if problems:
greatest_diff = get_problem_difficulty(problems[-1])

if greatest_diff < lower_bound:
i += 1
continue

for problem in problems:
tds = problem.find_all('td')

for tr in questions:
tds = tr.find_all('td')
diff = float(tds[8].text)
try:
diff = get_problem_difficulty(problem)
except ValueError:
continue

if diff > upbound:
end = True
break
if diff > upper_bound:
done = True

elif diff >= lobound and diff <= upbound:
idurl = tds[0].find('a')['href']
if idurl.split('/')[2] not in seenlist:
qlist.append(idurl.split('/')[2])
elif diff >= lower_bound and diff <= upper_bound:
qid_url = tds[0].find('a')['href']
if qid_url.split('/')[2] not in seenlist:
qlist.append(qid_url.split('/')[2])
i += 1

random.shuffle(qlist)
qlist = qlist[:n]

for j in range(min(n,len(qlist))):
os.popen('/' + os.getcwd() + '/fetch.py --id ' + qlist[j])
print(f'Here are {n} Kattis problems: {" ".join(qlist)}')

with open('seen.txt', 'a+') as f:
f.write('\n'.join(qlist) + '\n')
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
beautifulsoup4
requests
fake-useragent
22 changes: 12 additions & 10 deletions test.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
#!/usr/bin/env python3
import os
import sys
import shutil
import pathlib
import requests
import argparse

ISON_WINDOWS = sys.platform == 'win32'
EXE_NAME = 'a' if ISON_WINDOWS else './a.out'
WRITER_NAME = 'type' if ISON_WINDOWS else 'cat'
parser = argparse.ArgumentParser(description='Runs Kattis problem through their test cases')
parser.add_argument('--id', type=str, default='_NONE_', help='id of problem to fetch')
parser.add_argument('qid', metavar='ID', type=str, help='id of problem to fetch')
args = parser.parse_args()
qid = args.id
qid = args.qid

if qid == '_NONE_':
if qid == None:
qid = input('Enter ID: ')
qdir = os.path.join(os.getcwd(),qid)

PROBLEMS_PATH = './problems/'
PROBLEM_PATH = PROBLEMS_PATH + qid

qdir = os.path.join(PROBLEM_PATH)

qext = 'z'
while qext != 'c' and qext != 'p':
Expand All @@ -25,12 +25,14 @@

if qext == 'p':
EXE_NAME = '%s/_%s.py'%(qdir,qid)
else:
EXE_NAME = '%s/_%s.exe'%(qdir,qid) if ISON_WINDOWS else '%s/_%s'%(qdir,qid)

isinfile_inqdir = lambda name: name.startswith("input") and os.path.isfile(os.path.join(qdir,name))
input_files = list(filter(isinfile_inqdir, os.listdir(qdir)))
ilen = len(input_files)

os.system('g++ \"' + qdir + '/_' + qid + '.cpp\"')
os.system('g++ "' + qdir + '/_' + qid + '.cpp" -o ' + EXE_NAME)

for i in range(ilen):
print('TEST CASE ' + str(i+1))
Expand All @@ -44,4 +46,4 @@
if content==output:
print('PASSED')
else:
print('FAILED')
print('FAILED')