diff --git a/Dockerfile b/Dockerfile
index 699e279344..2c71a46068 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
-FROM ubuntu:20.04
+FROM ubuntu:24.04
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
build-essential \
@@ -7,7 +7,7 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
cppreference-doc-en-html \
fp-compiler \
git \
- haskell-platform \
+ ghc \
libcap-dev \
libcups2-dev \
libffi-dev \
@@ -15,13 +15,13 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
libyaml-dev \
mono-mcs \
openjdk-8-jdk-headless \
- php7.4-cli \
+ php-cli \
postgresql-client \
- python2 \
python3-pip \
- python3.8 \
- python3.8-dev \
+ python3.12 \
+ python3.12-dev \
rustc \
+ shared-mime-info \
sudo \
wait-for-it \
zip
@@ -39,8 +39,8 @@ COPY --chown=cmsuser:cmsuser requirements.txt dev-requirements.txt /home/cmsuser
WORKDIR /home/cmsuser/cms
-RUN sudo pip3 install -r requirements.txt
-RUN sudo pip3 install -r dev-requirements.txt
+RUN sudo pip3 install --break-system-packages -r requirements.txt
+RUN sudo pip3 install --break-system-packages -r dev-requirements.txt
COPY --chown=cmsuser:cmsuser . /home/cmsuser/cms
diff --git a/cms/io/web_service.py b/cms/io/web_service.py
index 41d2e2829c..21e1580514 100644
--- a/cms/io/web_service.py
+++ b/cms/io/web_service.py
@@ -21,6 +21,13 @@
import logging
+import collections
+try:
+ collections.MutableMapping
+except:
+ # Monkey-patch: Tornado 4.5.3 does not work on Python 3.11 by default
+ collections.MutableMapping = collections.abc.MutableMapping
+
try:
import tornado4.wsgi as tornado_wsgi
except ImportError:
diff --git a/cms/server/admin/handlers/base.py b/cms/server/admin/handlers/base.py
index e38be1fbbf..43df71b4b8 100644
--- a/cms/server/admin/handlers/base.py
+++ b/cms/server/admin/handlers/base.py
@@ -34,6 +34,13 @@
from datetime import datetime, timedelta
from functools import wraps
+import collections
+try:
+ collections.MutableMapping
+except:
+ # Monkey-patch: Tornado 4.5.3 does not work on Python 3.11 by default
+ collections.MutableMapping = collections.abc.MutableMapping
+
try:
import tornado4.web as tornado_web
except ImportError:
diff --git a/cms/server/admin/handlers/contestannouncement.py b/cms/server/admin/handlers/contestannouncement.py
index 464ff0cced..20f87fe3d5 100644
--- a/cms/server/admin/handlers/contestannouncement.py
+++ b/cms/server/admin/handlers/contestannouncement.py
@@ -26,6 +26,13 @@
"""
+import collections
+try:
+ collections.MutableMapping
+except:
+ # Monkey-patch: Tornado 4.5.3 does not work on Python 3.11 by default
+ collections.MutableMapping = collections.abc.MutableMapping
+
try:
import tornado4.web as tornado_web
except ImportError:
diff --git a/cms/server/admin/handlers/contestquestion.py b/cms/server/admin/handlers/contestquestion.py
index 625248f376..57ad1f13b7 100644
--- a/cms/server/admin/handlers/contestquestion.py
+++ b/cms/server/admin/handlers/contestquestion.py
@@ -27,6 +27,13 @@
import logging
+import collections
+try:
+ collections.MutableMapping
+except:
+ # Monkey-patch: Tornado 4.5.3 does not work on Python 3.11 by default
+ collections.MutableMapping = collections.abc.MutableMapping
+
try:
import tornado4.web as tornado_web
except ImportError:
diff --git a/cms/server/admin/handlers/contestuser.py b/cms/server/admin/handlers/contestuser.py
index 1706ce8f50..9ec869039b 100644
--- a/cms/server/admin/handlers/contestuser.py
+++ b/cms/server/admin/handlers/contestuser.py
@@ -31,6 +31,13 @@
import logging
+import collections
+try:
+ collections.MutableMapping
+except:
+ # Monkey-patch: Tornado 4.5.3 does not work on Python 3.11 by default
+ collections.MutableMapping = collections.abc.MutableMapping
+
try:
import tornado4.web as tornado_web
except ImportError:
diff --git a/cms/server/admin/handlers/dataset.py b/cms/server/admin/handlers/dataset.py
index d01716a90c..5adb03d093 100644
--- a/cms/server/admin/handlers/dataset.py
+++ b/cms/server/admin/handlers/dataset.py
@@ -32,6 +32,13 @@
import re
import zipfile
+import collections
+try:
+ collections.MutableMapping
+except:
+ # Monkey-patch: Tornado 4.5.3 does not work on Python 3.11 by default
+ collections.MutableMapping = collections.abc.MutableMapping
+
try:
import tornado4.web as tornado_web
except ImportError:
diff --git a/cms/server/admin/handlers/task.py b/cms/server/admin/handlers/task.py
index ff5d5e8e48..c9df69ff08 100644
--- a/cms/server/admin/handlers/task.py
+++ b/cms/server/admin/handlers/task.py
@@ -29,6 +29,13 @@
import logging
import traceback
+import collections
+try:
+ collections.MutableMapping
+except:
+ # Monkey-patch: Tornado 4.5.3 does not work on Python 3.11 by default
+ collections.MutableMapping = collections.abc.MutableMapping
+
try:
import tornado4.web as tornado_web
except ImportError:
diff --git a/cms/server/contest/handlers/base.py b/cms/server/contest/handlers/base.py
index 00e24c273f..faeb031f58 100644
--- a/cms/server/contest/handlers/base.py
+++ b/cms/server/contest/handlers/base.py
@@ -32,6 +32,13 @@
import logging
import traceback
+import collections
+try:
+ collections.MutableMapping
+except:
+ # Monkey-patch: Tornado 4.5.3 does not work on Python 3.11 by default
+ collections.MutableMapping = collections.abc.MutableMapping
+
try:
import tornado4.web as tornado_web
except ImportError:
diff --git a/cms/server/contest/handlers/communication.py b/cms/server/contest/handlers/communication.py
index 8bac98e539..572b13be64 100644
--- a/cms/server/contest/handlers/communication.py
+++ b/cms/server/contest/handlers/communication.py
@@ -29,6 +29,13 @@
import logging
+import collections
+try:
+ collections.MutableMapping
+except:
+ # Monkey-patch: Tornado 4.5.3 does not work on Python 3.11 by default
+ collections.MutableMapping = collections.abc.MutableMapping
+
try:
import tornado4.web as tornado_web
except ImportError:
diff --git a/cms/server/contest/handlers/contest.py b/cms/server/contest/handlers/contest.py
index 4fe6936ec4..724d5886b8 100644
--- a/cms/server/contest/handlers/contest.py
+++ b/cms/server/contest/handlers/contest.py
@@ -32,6 +32,13 @@
import ipaddress
import logging
+import collections
+try:
+ collections.MutableMapping
+except:
+ # Monkey-patch: Tornado 4.5.3 does not work on Python 3.11 by default
+ collections.MutableMapping = collections.abc.MutableMapping
+
try:
import tornado4.web as tornado_web
except ImportError:
diff --git a/cms/server/contest/handlers/main.py b/cms/server/contest/handlers/main.py
index 6d2fb2a51d..3ea2129d01 100644
--- a/cms/server/contest/handlers/main.py
+++ b/cms/server/contest/handlers/main.py
@@ -33,6 +33,13 @@
import logging
import re
+import collections
+try:
+ collections.MutableMapping
+except:
+ # Monkey-patch: Tornado 4.5.3 does not work on Python 3.11 by default
+ collections.MutableMapping = collections.abc.MutableMapping
+
try:
import tornado4.web as tornado_web
except ImportError:
diff --git a/cms/server/contest/handlers/task.py b/cms/server/contest/handlers/task.py
index 3f6f163a28..cfdfe1b69a 100644
--- a/cms/server/contest/handlers/task.py
+++ b/cms/server/contest/handlers/task.py
@@ -30,6 +30,13 @@
import logging
+import collections
+try:
+ collections.MutableMapping
+except:
+ # Monkey-patch: Tornado 4.5.3 does not work on Python 3.11 by default
+ collections.MutableMapping = collections.abc.MutableMapping
+
try:
import tornado4.web as tornado_web
except ImportError:
diff --git a/cms/server/contest/handlers/tasksubmission.py b/cms/server/contest/handlers/tasksubmission.py
index 9199c0bcd7..e233291a76 100644
--- a/cms/server/contest/handlers/tasksubmission.py
+++ b/cms/server/contest/handlers/tasksubmission.py
@@ -32,6 +32,13 @@
import logging
import re
+import collections
+try:
+ collections.MutableMapping
+except:
+ # Monkey-patch: Tornado 4.5.3 does not work on Python 3.11 by default
+ collections.MutableMapping = collections.abc.MutableMapping
+
try:
import tornado4.web as tornado_web
except ImportError:
diff --git a/cms/server/contest/handlers/taskusertest.py b/cms/server/contest/handlers/taskusertest.py
index c78c87c1c8..07da1f9a59 100644
--- a/cms/server/contest/handlers/taskusertest.py
+++ b/cms/server/contest/handlers/taskusertest.py
@@ -31,6 +31,13 @@
import logging
import re
+import collections
+try:
+ collections.MutableMapping
+except:
+ # Monkey-patch: Tornado 4.5.3 does not work on Python 3.11 by default
+ collections.MutableMapping = collections.abc.MutableMapping
+
try:
import tornado4.web as tornado_web
except ImportError:
diff --git a/cms/server/util.py b/cms/server/util.py
index 953d5eb1c3..7b787595cb 100644
--- a/cms/server/util.py
+++ b/cms/server/util.py
@@ -30,6 +30,13 @@
from functools import wraps
from urllib.parse import quote, urlencode
+import collections
+try:
+ collections.MutableMapping
+except:
+ # Monkey-patch: Tornado 4.5.3 does not work on Python 3.11 by default
+ collections.MutableMapping = collections.abc.MutableMapping
+
try:
from tornado4.web import RequestHandler
except ImportError:
diff --git a/cmscontrib/loaders/polygon.py b/cmscontrib/loaders/polygon.py
index fb4a4b4ade..200dd38c8d 100644
--- a/cmscontrib/loaders/polygon.py
+++ b/cmscontrib/loaders/polygon.py
@@ -20,7 +20,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import imp
import logging
import os
import subprocess
@@ -145,10 +144,11 @@ def get_task(self, get_statement=True):
task_cms_conf = None
if os.path.exists(task_cms_conf_path):
logger.info("Found additional CMS options for task %s.", name)
- with open(task_cms_conf_path, 'rb') as f:
- task_cms_conf = imp.load_module('cms_conf', f,
- task_cms_conf_path,
- ('.py', 'r', imp.PY_SOURCE))
+ import importlib.util
+ spec = importlib.util.spec_from_file_location(
+ 'cms_conf', task_cms_conf_path)
+ task_cms_conf = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(task_cms_conf)
if task_cms_conf is not None and hasattr(task_cms_conf, "general"):
args.update(task_cms_conf.general)
diff --git a/cmstestsuite/unit_tests/grading/ParameterTypesTest.py b/cmstestsuite/unit_tests/grading/ParameterTypesTest.py
index 21e20ec38a..c7c56af41b 100755
--- a/cmstestsuite/unit_tests/grading/ParameterTypesTest.py
+++ b/cmstestsuite/unit_tests/grading/ParameterTypesTest.py
@@ -20,6 +20,13 @@
import unittest
+import collections
+try:
+ collections.MutableMapping
+except:
+ # Monkey-patch: Tornado 4.5.3 does not work on Python 3.11 by default
+ collections.MutableMapping = collections.abc.MutableMapping
+
try:
from tornado4.web import MissingArgumentError
except ImportError:
diff --git a/cmstestsuite/unit_tests/locale/locale_test.py b/cmstestsuite/unit_tests/locale/locale_test.py
index 170a488473..e81d0fe458 100755
--- a/cmstestsuite/unit_tests/locale/locale_test.py
+++ b/cmstestsuite/unit_tests/locale/locale_test.py
@@ -103,20 +103,20 @@ def test_utc(self):
self.assertEqual(
ENGLISH.format_datetime(datetime(2018, 1, 1, 12, 34, 56),
timezone=UTC),
- "Jan 1, 2018, 12:34:56 PM")
+ "Jan 1, 2018, 12:34:56\N{Narrow No-Break Space}PM")
def test_other_timezone_winter(self):
# Other timezone, in winter (no DST).
self.assertEqual(
ENGLISH.format_datetime(datetime(2018, 1, 1, 12, 34, 56),
timezone=ROME),
- "Jan 1, 2018, 1:34:56 PM")
+ "Jan 1, 2018, 1:34:56\N{Narrow No-Break Space}PM")
def test_other_timezone_summer(self):
self.assertEqual(
ENGLISH.format_datetime(datetime(2018, 7, 1, 12, 34, 56),
timezone=ROME),
- "Jul 1, 2018, 2:34:56 PM")
+ "Jul 1, 2018, 2:34:56\N{Narrow No-Break Space}PM")
# As above, localized (use a language with a 24h clock and with a
# different day/month/year order).
@@ -146,19 +146,19 @@ def test_utc(self):
# UTC, in English
self.assertEqual(ENGLISH.format_time(datetime(2018, 1, 1, 12, 34, 56),
timezone=UTC),
- "12:34:56 PM")
+ "12:34:56\N{Narrow No-Break Space}PM")
def test_other_timezone_winter(self):
# Other timezone, in winter (no DST).
self.assertEqual(ENGLISH.format_time(datetime(2018, 1, 1, 12, 34, 56),
timezone=ROME),
- "1:34:56 PM")
+ "1:34:56\N{Narrow No-Break Space}PM")
def test_other_timezone_in_summer(self):
# Other timezone, in summer (DST).
self.assertEqual(ENGLISH.format_time(datetime(2018, 7, 1, 12, 34, 56),
timezone=ROME),
- "2:34:56 PM")
+ "2:34:56\N{Narrow No-Break Space}PM")
# As above, localized (use Danish as they use periods rather
# than colons and have a 24h clock).
@@ -187,17 +187,17 @@ def test_utc(self):
ENGLISH.format_datetime_smart(datetime(2018, 1, 1, 12, 34, 56),
datetime(2018, 1, 1, 23, 30),
timezone=UTC),
- "12:34:56 PM")
+ "12:34:56\N{Narrow No-Break Space}PM")
self.assertEqual(
ENGLISH.format_datetime_smart(datetime(2018, 1, 1, 12, 34, 56),
datetime(2018, 1, 2, 0, 30),
timezone=UTC),
- "Jan 1, 2018, 12:34:56 PM")
+ "Jan 1, 2018, 12:34:56\N{Narrow No-Break Space}PM")
self.assertEqual(
ENGLISH.format_datetime_smart(datetime(2018, 1, 1, 12, 34, 56),
datetime(2017, 12, 31, 23, 30),
timezone=UTC),
- "Jan 1, 2018, 12:34:56 PM")
+ "Jan 1, 2018, 12:34:56\N{Narrow No-Break Space}PM")
def test_other_timezone_winter(self):
# Other timezone, in winter (no DST).
@@ -205,17 +205,17 @@ def test_other_timezone_winter(self):
ENGLISH.format_datetime_smart(datetime(2018, 1, 1, 12, 34, 56),
datetime(2018, 1, 1, 22, 30),
timezone=ROME),
- "1:34:56 PM")
+ "1:34:56\N{Narrow No-Break Space}PM")
self.assertEqual(
ENGLISH.format_datetime_smart(datetime(2018, 1, 1, 12, 34, 56),
datetime(2018, 1, 1, 23, 30),
timezone=ROME),
- "Jan 1, 2018, 1:34:56 PM")
+ "Jan 1, 2018, 1:34:56\N{Narrow No-Break Space}PM")
self.assertEqual(
ENGLISH.format_datetime_smart(datetime(2018, 1, 1, 12, 34, 56),
datetime(2017, 12, 31, 22, 30),
timezone=ROME),
- "Jan 1, 2018, 1:34:56 PM")
+ "Jan 1, 2018, 1:34:56\N{Narrow No-Break Space}PM")
def test_other_timezone_summer(self):
# Other timezone, in summer (DST).
@@ -223,17 +223,17 @@ def test_other_timezone_summer(self):
ENGLISH.format_datetime_smart(datetime(2018, 7, 1, 12, 34, 56),
datetime(2018, 7, 1, 21, 30),
timezone=ROME),
- "2:34:56 PM")
+ "2:34:56\N{Narrow No-Break Space}PM")
self.assertEqual(
ENGLISH.format_datetime_smart(datetime(2018, 7, 1, 12, 34, 56),
datetime(2018, 7, 1, 22, 30),
timezone=ROME),
- "Jul 1, 2018, 2:34:56 PM")
+ "Jul 1, 2018, 2:34:56\N{Narrow No-Break Space}PM")
self.assertEqual(
ENGLISH.format_datetime_smart(datetime(2018, 7, 1, 12, 34, 56),
datetime(2018, 6, 30, 21, 30),
timezone=ROME),
- "Jul 1, 2018, 2:34:56 PM")
+ "Jul 1, 2018, 2:34:56\N{Narrow No-Break Space}PM")
# As above, localized.
@@ -508,23 +508,23 @@ def test_large(self):
def test_localized_zero(self):
self.assertEqual(FRENCH.format_size(0),
- "0 octet")
+ "0\N{No-Break Space}octet")
def test_localized_small_values(self):
self.assertEqual(FRENCH.format_size(1),
- "1 octet")
+ "1\N{No-Break Space}octet")
self.assertEqual(FRENCH.format_size(2),
- "2 octets")
+ "2\N{No-Break Space}octets")
def test_localized_cutoff_kib(self):
self.assertEqual(FRENCH.format_size(999),
- "999 octets")
+ "999\N{No-Break Space}octets")
self.assertEqual(FRENCH.format_size(1000),
- "1\N{NO-BREAK SPACE}000 octets")
+ "1\N{Narrow No-Break Space}000\N{No-Break Space}octets")
self.assertEqual(FRENCH.format_size(1001),
- "1\N{NO-BREAK SPACE}001 octets")
+ "1\N{Narrow No-Break Space}001\N{No-Break Space}octets")
self.assertEqual(FRENCH.format_size(1023),
- "1\N{NO-BREAK SPACE}023 octets")
+ "1\N{Narrow No-Break Space}023\N{No-Break Space}octets")
self.assertEqual(FRENCH.format_size(1024),
"1,00 Kio")
self.assertEqual(FRENCH.format_size(1025),
@@ -534,17 +534,16 @@ def test_localized_cutoff_mib(self):
self.assertEqual(FRENCH.format_size(999 * 1024),
"999 Kio")
self.assertEqual(FRENCH.format_size(1000 * 1024),
- "1\N{NO-BREAK SPACE}000 Kio")
+ "1\N{Narrow No-Break Space}000 Kio")
self.assertEqual(FRENCH.format_size(1001 * 1024),
- "1\N{NO-BREAK SPACE}001 Kio")
+ "1\N{Narrow No-Break Space}001 Kio")
self.assertEqual(FRENCH.format_size(1023 * 1024),
- "1\N{NO-BREAK SPACE}023 Kio")
+ "1\N{Narrow No-Break Space}023 Kio")
self.assertEqual(FRENCH.format_size(1024 * 1024),
"1,00 Mio")
self.assertEqual(FRENCH.format_size(1025 * 1024),
"1,00 Mio")
-
def test_localized_large(self):
self.assertEqual(FRENCH.format_size(2_345_000),
"2,24 Mio")
@@ -553,7 +552,7 @@ def test_localized_large(self):
self.assertEqual(FRENCH.format_size(456_789_000_000_000),
"415 Tio")
self.assertEqual(FRENCH.format_size(5_678_912_300_000_000),
- "5\N{NO-BREAK SPACE}165 Tio")
+ "5\N{Narrow No-Break Space}165 Tio")
class TestFormatDecimal(unittest.TestCase):
@@ -594,7 +593,7 @@ class TestTranslateMimetype(unittest.TestCase):
@unittest.skipIf(not os.path.isfile(
"/usr/share/locale/it/LC_MESSAGES/shared-mime-info.mo"),
- reason="need Italian shared-mime-info translation")
+ reason="need Italian shared-mime-info translation")
def test_translate_mimetype(self):
self.assertEqual(ENGLISH.translate_mimetype("PDF document"),
"PDF document")
diff --git a/codecov/.gitkeep b/codecov/.gitkeep
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/docs/Installation.rst b/docs/Installation.rst
index 9ad179c2db..bf92fb67d3 100644
--- a/docs/Installation.rst
+++ b/docs/Installation.rst
@@ -49,7 +49,7 @@ All dependencies can be installed automatically on most Linux distributions.
Ubuntu
------
-On Ubuntu 20.04, one will need to run the following script to satisfy all dependencies:
+On Ubuntu 24.04, one will need to run the following script to satisfy all dependencies:
.. sourcecode:: bash
@@ -63,8 +63,8 @@ On Ubuntu 20.04, one will need to run the following script to satisfy all depend
libffi-dev python3-pip
# Optional
- sudo apt-get install nginx-full python2.7 php7.4-cli php7.4-fpm \
- phppgadmin texlive-latex-base a2ps haskell-platform rustc mono-mcs
+ sudo apt-get install nginx-full python3 php-cli texlive-latex-base \
+ a2ps ghc rustc mono-mcs
The above commands provide a very essential Pascal environment. Consider installing the following packages for additional units: `fp-units-base`, `fp-units-fcl`, `fp-units-misc`, `fp-units-math` and `fp-units-rtl`.
@@ -86,7 +86,7 @@ On Arch Linux, unofficial AUR packages can be found: `cms =4.5,<4.6 # http://www.tornadoweb.org/en/stable/releases.html
-psycopg2>=2.8,<2.9 # http://initd.org/psycopg/articles/tag/release/
+tornado==4.5.3 # http://www.tornadoweb.org/en/stable/releases.html
+psycopg2==2.9.7 # http://initd.org/psycopg/articles/tag/release/
sqlalchemy>=1.3,<1.4 # http://docs.sqlalchemy.org/en/latest/changelog/index.html
netifaces>=0.10,<0.11 # https://bitbucket.org/al45tair/netifaces/src/
-pycryptodomex>=3.6,<3.7 # https://github.com/Legrandin/pycryptodome/blob/master/Changelog.rst
+pycryptodomex==3.19.0 # https://github.com/Legrandin/pycryptodome/blob/master/Changelog.rst
psutil>=5.5,<5.6 # https://github.com/giampaolo/psutil/blob/master/HISTORY.rst
-requests>=2.22,<2.23 # https://pypi.python.org/pypi/requests
-gevent==20.12.0 # http://www.gevent.org/changelog.html
-# Limit greenlet version for binary compatibility with gevent 20.12 wheels
-greenlet==1.0.0
-werkzeug>=0.16,<0.17 # https://github.com/pallets/werkzeug/blob/master/CHANGES
+requests==2.32.3 # https://pypi.python.org/pypi/requests
+gevent==23.9.0.post1 # http://www.gevent.org/changelog.html
+werkzeug<1.0 # https://github.com/pallets/werkzeug/blob/master/CHANGES
+backports.ssl-match-hostname==3.7.0.1 # required by tornado<5.0
+greenlet>=3.0rc1
patool>=1.12,<1.13 # https://github.com/wummel/patool/blob/master/doc/changelog.txt
bcrypt>=3.1,<3.2 # https://github.com/pyca/bcrypt/
chardet>=3.0,<3.1 # https://pypi.python.org/pypi/chardet
-babel>=2.6,<2.7 # http://babel.pocoo.org/en/latest/changelog.html
+babel==2.12.1 # http://babel.pocoo.org/en/latest/changelog.html
pyxdg>=0.26,<0.27 # https://freedesktop.org/wiki/Software/pyxdg/
Jinja2>=2.10,<2.11 # http://jinja.pocoo.org/docs/latest/changelog/
@@ -26,5 +24,5 @@ MarkupSafe==2.0.1
pyyaml>=5.3,<5.4 # http://pyyaml.org/wiki/PyYAML
# Only for printing:
-pycups>=1.9,<1.10 # https://pypi.python.org/pypi/pycups
+pycups==2.0.4 # https://pypi.python.org/pypi/pycups
PyPDF2>=1.26,<1.27 # https://github.com/mstamy2/PyPDF2/blob/master/CHANGELOG