diff options
-rw-r--r-- | .coveragerc | 13 | ||||
-rw-r--r-- | .dockerignore | 9 | ||||
-rw-r--r-- | .github/workflows/build.yaml | 24 | ||||
-rw-r--r-- | .github/workflows/deploy.yaml | 15 | ||||
-rw-r--r-- | .github/workflows/main.yaml | 4 | ||||
-rw-r--r-- | .github/workflows/test.yaml | 9 | ||||
-rw-r--r-- | Dockerfile | 21 | ||||
-rw-r--r-- | Makefile | 6 | ||||
-rw-r--r-- | config/gunicorn.conf.py | 3 | ||||
-rw-r--r-- | docker-compose.yml | 3 | ||||
-rw-r--r-- | pyproject.toml | 66 | ||||
-rw-r--r-- | requirements/requirements.in | 7 | ||||
-rw-r--r-- | requirements/requirements.pip | 12 | ||||
-rw-r--r-- | scripts/version.py | 38 | ||||
-rw-r--r-- | snekbox/__init__.py | 55 | ||||
-rw-r--r-- | snekbox/__main__.py | 2 | ||||
-rw-r--r-- | snekbox/api/app.py | 3 | ||||
-rw-r--r-- | snekbox/api/resources/eval.py | 2 | ||||
-rw-r--r-- | snekbox/nsjail.py | 2 | ||||
-rw-r--r-- | snekbox/utils/__init__.py | 4 | ||||
-rw-r--r-- | snekbox/utils/cgroup.py | 2 | ||||
-rw-r--r-- | snekbox/utils/gunicorn.py | 33 | ||||
-rw-r--r-- | snekbox/utils/logging.py | 35 | ||||
-rw-r--r-- | snekbox/utils/swap.py | 2 | ||||
-rw-r--r-- | tests/gunicorn_utils.py | 4 |
25 files changed, 255 insertions, 119 deletions
diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index ce475d7..0000000 --- a/.coveragerc +++ /dev/null @@ -1,13 +0,0 @@ -[run] -branch = true -data_file = ${COVERAGE_DATAFILE-.coverage} -include = snekbox/* -omit = - snekbox/api/app.py - snekbox/config_pb2.py -relative_files = true - -[report] -exclude_lines = - pragma: no cover - if DEBUG diff --git a/.dockerignore b/.dockerignore index 30ecfd4..6a360ff 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,8 +2,13 @@ * # Make exceptions for what's needed -!snekbox +!.git/ !config/ !requirements/ -!tests +!scripts/ +!snekbox/ +!tests/ !LICENSE +!NOTICE +!pyproject.toml +!README.md diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 28e5b69..e5791c9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -4,9 +4,9 @@ on: artifact: description: The name of the uploaded image aretfact. value: ${{ jobs.build.outputs.artifact }} - tag: - description: The tag used for the built image. - value: ${{ jobs.build.outputs.tag }} + version: + description: The package's version. + value: ${{ jobs.build.outputs.version }} jobs: build: @@ -14,21 +14,21 @@ jobs: runs-on: ubuntu-latest outputs: artifact: ${{ env.artifact }} - tag: ${{ steps.sha_tag.outputs.tag }} + version: ${{ steps.version.outputs.version }} env: artifact: image_artifact_snekbox-venv steps: - # Create a short SHA with which to tag built images. - - name: Create SHA Container Tag - id: sha_tag - run: | - tag=$(cut -c 1-7 <<< $GITHUB_SHA) - echo "::set-output name=tag::$tag" - - name: Checkout code uses: actions/checkout@v2 + - name: Get version + id: version + run: | + set -eu + version=$(python scripts/version.py) + echo "::set-output name=version::version" + # The current version (v2) of Docker's build-push action uses buildx, # which comes with BuildKit. It has cache features which can speed up # the builds. See https://github.com/docker/build-push-action @@ -83,7 +83,7 @@ jobs: ghcr.io/python-discord/snekbox-base:latest ghcr.io/python-discord/snekbox-venv:latest cache-to: ${{ steps.cache_config.outputs.cache_to }} - tags: ghcr.io/python-discord/snekbox-venv:${{ steps.sha_tag.outputs.tag }} + tags: ghcr.io/python-discord/snekbox-venv:${{ steps.version.outputs.version }} # Make the image available as an artifact so other jobs will be able to # download it. diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 3b12921..9113188 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -4,7 +4,7 @@ on: artifact: required: true type: string - tag: + version: required: true type: string secrets: @@ -55,8 +55,7 @@ jobs: cache-to: type=inline tags: | ghcr.io/python-discord/snekbox:latest - ghcr.io/python-discord/snekbox:${{ inputs.tag }} - build-args: git_sha=${{ github.sha }} + ghcr.io/python-discord/snekbox:${{ inputs.version }} # Deploy to Kubernetes. - name: Authenticate with Kubernetes @@ -69,7 +68,7 @@ jobs: uses: Azure/k8s-deploy@v1 with: manifests: deployment.yaml - images: 'ghcr.io/python-discord/snekbox:${{ inputs.tag }}' + images: 'ghcr.io/python-discord/snekbox:${{ inputs.version }}' kubectl-version: 'latest' # Push the base image to GHCR, with an inline cache manifest. @@ -82,7 +81,9 @@ jobs: push: true cache-from: ghcr.io/python-discord/snekbox-base:latest cache-to: type=inline - tags: ghcr.io/python-discord/snekbox-base:latest + tags: | + ghcr.io/python-discord/snekbox-base:latest + ghcr.io/python-discord/snekbox-base:${{ inputs.version }} # Push the venv image to GHCR, with an inline cache manifest. - name: Push venv image @@ -96,4 +97,6 @@ jobs: ghcr.io/python-discord/snekbox-base:latest ghcr.io/python-discord/snekbox-venv:latest cache-to: type=inline - tags: ghcr.io/python-discord/snekbox-venv:latest + tags: | + ghcr.io/python-discord/snekbox-venv:latest + ghcr.io/python-discord/snekbox-venv:${{ inputs.version }} diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index f28ab61..b581ba3 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -16,13 +16,13 @@ jobs: needs: build with: artifact: ${{ needs.build.outputs.artifact }} - tag: ${{ needs.build.outputs.tag }} + version: ${{ needs.build.outputs.version }} deploy: uses: ./.github/workflows/deploy.yaml if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} needs: [build, lint, test] with: artifact: ${{ needs.build.outputs.artifact }} - tag: ${{ needs.build.outputs.tag }} + version: ${{ needs.build.outputs.version }} secrets: KUBECONFIG: ${{ secrets.KUBECONFIG }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f7f3635..30e6ba3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -4,7 +4,7 @@ on: artifact: required: true type: string - tag: + version: required: true type: string @@ -38,11 +38,12 @@ jobs: - name: Run tests id: run_tests run: | - export IMAGE_SUFFIX='-venv:${{ inputs.tag }}' + export IMAGE_SUFFIX='-venv:${{ inputs.version }}' docker-compose run \ --rm -T -e COVERAGE_DATAFILE=.coverage.${{ matrix.os }} \ + --entrypoint coverage \ snekbox \ - coverage run -m unittest + run -m unittest # Upload it so the coverage from all matrix jobs can be combined later. - name: Upload coverage data @@ -57,7 +58,7 @@ jobs: - name: Docker cleanup if: matrix.os == 'self-hosted' && always() run: | - export IMAGE_SUFFIX='-venv:${{ inputs.tag }}' + export IMAGE_SUFFIX='-venv:${{ inputs.version }}' docker-compose down --rmi all --remove-orphans -v -t 0 report: @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1 FROM python:3.10-slim-buster as builder WORKDIR /nsjail @@ -31,6 +32,7 @@ ENV PATH=/root/.local/bin:$PATH \ RUN apt-get -y update \ && apt-get install -y \ gcc=4:8.3.* \ + git=1:2.20.* \ libnl-route-3-200=3.4.* \ libprotobuf17=3.6.* \ && rm -rf /var/lib/apt/lists/* @@ -60,18 +62,17 @@ RUN if [ -n "${DEV}" ]; \ fi # At the end to avoid re-installing dependencies when only a config changes. -# It's in the venv image because the final image is not used during development. COPY config/ /snekbox/config/ -# ------------------------------------------------------------------------------ -FROM venv - ENTRYPOINT ["gunicorn"] -CMD ["-c", "config/gunicorn.conf.py", "snekbox.api.app"] +CMD ["-c", "config/gunicorn.conf.py"] -COPY . /snekbox -WORKDIR /snekbox +# ------------------------------------------------------------------------------ +FROM venv -# At the end to prevent it from invalidating the layer cache. -ARG git_sha="development" -ENV GIT_SHA=$git_sha +# Use a separate directory to avoid importing the source over the installed pkg. +# The venv already installed dependencies, so nothing besides snekbox itself +# will be installed. Note requirements.pip cannot be used as a constraint file +# because it contains extras, which pip disallows. +RUN --mount=source=.,target=/snekbox_src,rw \ + pip install /snekbox_src[gunicorn,sentry] \ @@ -13,7 +13,8 @@ setup: install-piptools .PHONY: upgrade upgrade: install-piptools - $(PIP_COMPILE_CMD) -o requirements/requirements.pip requirements/requirements.in + $(PIP_COMPILE_CMD) -o requirements/requirements.pip \ + --extra gunicorn --extra sentry pyproject.toml $(PIP_COMPILE_CMD) -o requirements/coverage.pip requirements/coverage.in $(PIP_COMPILE_CMD) -o requirements/coveralls.pip requirements/coveralls.in $(PIP_COMPILE_CMD) -o requirements/lint.pip requirements/lint.in @@ -24,12 +25,11 @@ lint: setup pre-commit run --all-files # Fix ownership of the coverage file even if tests fail & preserve exit code -# Install numpy because a test checks if it's importable .PHONY: test test: docker-compose build -q --force-rm docker-compose run --entrypoint /bin/bash --rm snekbox -c \ - 'coverage run -m unittest; e=$?; chown --reference=. .coverage; exit $e' + 'coverage run -m unittest; e=$?; chown --reference=. .coverage; exit $e' .PHONY: report report: setup diff --git a/config/gunicorn.conf.py b/config/gunicorn.conf.py index 5ab11f4..563f8ea 100644 --- a/config/gunicorn.conf.py +++ b/config/gunicorn.conf.py @@ -1,5 +1,6 @@ workers = 2 bind = "0.0.0.0:8060" -logger_class = "snekbox.GunicornLogger" +logger_class = "snekbox.utils.gunicorn.GunicornLogger" access_logformat = "%(m)s %(U)s%(q)s %(s)s %(b)s %(L)ss" access_logfile = "-" +wsgi_app = "snekbox:SnekAPI" diff --git a/docker-compose.yml b/docker-compose.yml index 3854825..aa1a0f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: container_name: snekbox_dev hostname: snekbox_dev privileged: true - image: ghcr.io/python-discord/snekbox${IMAGE_SUFFIX:-:dev} + image: ghcr.io/python-discord/snekbox${IMAGE_SUFFIX:--venv:dev} ports: - 8060:8060 init: true @@ -17,6 +17,7 @@ services: build: context: . dockerfile: Dockerfile + target: venv args: DEV: 1 cache_from: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..08aeb8c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,66 @@ +[build-system] +requires = ["setuptools>=61", "setuptools-git-versioning>=1.8"] +build-backend = "setuptools.build_meta:__legacy__" + +[project] +name = "snekbox" +description = "HTTP REST API for sandboxed execution of arbitrary Python code." +readme = "README.md" +license = {text = "MIT"} +authors = [{name = "Python Discord", email = "[email protected]"}] +keywords = ["sandbox", "nsjail", "HTTP REST API"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + "Topic :: Security", + "Topic :: Software Development :: Interpreters", +] +dynamic = ["version"] + +requires-python = ">=3.10" +dependencies = [ + # Sentry's Falcon integration relies on api_helpers (falconry/falcon#1902). + "falcon>=3.0.1", + "jsonschema>=4.0", + "protobuf>=3.19", +] + +[project.optional-dependencies] +gunicorn = ["gunicorn>=20.1"] # Lowest which supports wsgi_app in config. +sentry = ["sentry-sdk[falcon]>=1.5.4"] + +[project.urls] +source = "https://github.com/python-discord/snekbox" +tracker = "https://github.com/python-discord/snekbox/issues" + +[project.scripts] +snekbox = "snekbox.__main__:main" + +[tool.setuptools.packages.find] +include = ["snekbox*"] + +[tool.setuptools-git-versioning] +enabled = true +version_callback = "scripts.version:get_version" + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "if DEBUG" +] + +[tool.coverage.run] +branch = true +data_file = "${COVERAGE_DATAFILE-.coverage}" +include = ["snekbox/*"] +omit = [ + "snekbox/api/app.py", + "snekbox/config_pb2.py" +] +relative_files = true diff --git a/requirements/requirements.in b/requirements/requirements.in deleted file mode 100644 index 775ad39..0000000 --- a/requirements/requirements.in +++ /dev/null @@ -1,7 +0,0 @@ -# Sentry's Falcon integration relies on api_helpers. See falconry/falcon#1902 -falcon>=3.0.1 - -gunicorn>=20 -jsonschema>=4.0 -protobuf>=3.19 -sentry-sdk[falcon]>=1.5.4 diff --git a/requirements/requirements.pip b/requirements/requirements.pip index 21b6678..034f104 100644 --- a/requirements/requirements.pip +++ b/requirements/requirements.pip @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with python 3.10 # To update, run: # -# pip-compile --output-file=requirements/requirements.pip requirements/requirements.in +# pip-compile --extra=gunicorn --extra=sentry --output-file=requirements/requirements.pip pyproject.toml # attrs==21.4.0 # via jsonschema @@ -10,18 +10,18 @@ certifi==2022.5.18.1 # via sentry-sdk falcon==3.1.0 # via - # -r requirements/requirements.in # sentry-sdk + # snekbox (pyproject.toml) gunicorn==20.1.0 - # via -r requirements/requirements.in + # via snekbox (pyproject.toml) jsonschema==4.5.1 - # via -r requirements/requirements.in + # via snekbox (pyproject.toml) protobuf==4.21.1 - # via -r requirements/requirements.in + # via snekbox (pyproject.toml) pyrsistent==0.18.1 # via jsonschema sentry-sdk[falcon]==1.5.12 - # via -r requirements/requirements.in + # via snekbox (pyproject.toml) urllib3==1.26.9 # via sentry-sdk diff --git a/scripts/version.py b/scripts/version.py new file mode 100644 index 0000000..bf8d509 --- /dev/null +++ b/scripts/version.py @@ -0,0 +1,38 @@ +import datetime +import subprocess + +__all__ = ("get_version",) + + +def get_version() -> str: + """ + Return a version based on the HEAD commit's date. + + The format is 'year.month.day.commits' and is compliant with PEP 440. 'commits' is the amount of + commits made on the same date as HEAD, excluding HEAD. This ensures versions are unique if + multiple release occur on the same date. + """ + args = ["git", "show", "-s", "--format=%ct", "HEAD"] + stdout = subprocess.check_output(args, text=True) + timestamp = float(stdout.strip()) + date = datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc) + + commits = count_commits_on_date(date) - 1 # Exclude HEAD. + + # Don't use strftime because it includes leading zeros, which are against PEP 440. + return f"{date.year}.{date.month}.{date.day}.{commits}" + + +def count_commits_on_date(dt: datetime.datetime) -> int: + """Return the amount of commits made on the given UTC aware datetime.""" + dt = dt.combine(dt - datetime.timedelta(days=1), dt.max.time(), dt.tzinfo) + + # git log uses the committer date for this, not the author date. + args = ["git", "log", "--oneline", "--after", str(dt.timestamp())] + stdout = subprocess.check_output(args, text=True) + + return stdout.strip().count("\n") + + +if __name__ == "__main__": + print(get_version()) diff --git a/snekbox/__init__.py b/snekbox/__init__.py index 42628d4..657c032 100644 --- a/snekbox/__init__.py +++ b/snekbox/__init__.py @@ -1,51 +1,18 @@ -import logging import os -import sys - -import sentry_sdk -from gunicorn import glogging -from gunicorn.config import Config -from sentry_sdk.integrations.falcon import FalconIntegration +from importlib import metadata DEBUG = os.environ.get("DEBUG", False) -GIT_SHA = os.environ.get("GIT_SHA", "development") - -sentry_sdk.init( - dsn=os.environ.get("SNEKBOX_SENTRY_DSN", ""), - integrations=[FalconIntegration()], - send_default_pii=True, - release=f"snekbox@{GIT_SHA}" -) - - -class GunicornLogger(glogging.Logger): - """Logger for Gunicorn with custom formatting and support for the DEBUG environment variable.""" - - error_fmt = "%(asctime)s | %(process)5s | %(name)30s | %(levelname)8s | %(message)s" - access_fmt = error_fmt - datefmt = None # Use the default ISO 8601 format - - def setup(self, cfg: Config) -> None: - """ - Set up loggers and set error logger's level to DEBUG if the DEBUG env var is set. - - Note: Access and syslog handlers would need to be recreated to use a custom date format - because they are created with an unspecified datefmt argument by default. - """ - super().setup(cfg) - if DEBUG: - self.loglevel = logging.DEBUG - else: - self.loglevel = self.LOG_LEVELS.get(cfg.loglevel.lower(), logging.INFO) +try: + __version__ = metadata.version("snekbox") +except metadata.PackageNotFoundError: # pragma: no cover + __version__ = "0.0.0.0+unknown" - self.error_log.setLevel(self.loglevel) +from snekbox.api import SnekAPI # noqa: E402 +from snekbox.nsjail import NsJail # noqa: E402 +from snekbox.utils.logging import init_logger, init_sentry # noqa: E402 +__all__ = ("NsJail", "SnekAPI") -log = logging.getLogger("snekbox") -log.setLevel(logging.DEBUG if DEBUG else logging.INFO) -log.propagate = True -formatter = logging.Formatter(GunicornLogger.error_fmt) -handler = logging.StreamHandler(sys.stdout) -handler.setFormatter(formatter) -log.addHandler(handler) +init_sentry(__version__) +init_logger(DEBUG) diff --git a/snekbox/__main__.py b/snekbox/__main__.py index 704ec9d..7ac10e9 100644 --- a/snekbox/__main__.py +++ b/snekbox/__main__.py @@ -1,7 +1,7 @@ import argparse import sys -from snekbox.nsjail import NsJail +from snekbox import NsJail def parse_args() -> argparse.Namespace: diff --git a/snekbox/api/app.py b/snekbox/api/app.py deleted file mode 100644 index c71e246..0000000 --- a/snekbox/api/app.py +++ /dev/null @@ -1,3 +0,0 @@ -from . import SnekAPI - -application = SnekAPI() diff --git a/snekbox/api/resources/eval.py b/snekbox/api/resources/eval.py index 9560d0b..0a59f2e 100644 --- a/snekbox/api/resources/eval.py +++ b/snekbox/api/resources/eval.py @@ -5,6 +5,8 @@ from falcon.media.validators.jsonschema import validate from snekbox.nsjail import NsJail +__all__ = ("EvalResource",) + log = logging.getLogger(__name__) diff --git a/snekbox/nsjail.py b/snekbox/nsjail.py index ac36551..1aca637 100644 --- a/snekbox/nsjail.py +++ b/snekbox/nsjail.py @@ -13,6 +13,8 @@ from google.protobuf import text_format from snekbox import DEBUG, utils from snekbox.config_pb2 import NsJailConfig +__all__ = ("NsJail",) + log = logging.getLogger(__name__) # [level][timestamp][PID]? function_signature:line_no? message diff --git a/snekbox/utils/__init__.py b/snekbox/utils/__init__.py index 5a7b632..6d6bc32 100644 --- a/snekbox/utils/__init__.py +++ b/snekbox/utils/__init__.py @@ -1,3 +1,3 @@ -from . import cgroup, swap +from . import cgroup, logging, swap -__all__ = ("cgroup", "swap") +__all__ = ("cgroup", "logging", "swap") diff --git a/snekbox/utils/cgroup.py b/snekbox/utils/cgroup.py index 3e12406..cd515ab 100644 --- a/snekbox/utils/cgroup.py +++ b/snekbox/utils/cgroup.py @@ -5,6 +5,8 @@ from snekbox.config_pb2 import NsJailConfig log = logging.getLogger(__name__) +__all__ = ("get_version", "init", "init_v1", "init_v2") + def get_version(config: NsJailConfig) -> int: """ diff --git a/snekbox/utils/gunicorn.py b/snekbox/utils/gunicorn.py new file mode 100644 index 0000000..68e3ed9 --- /dev/null +++ b/snekbox/utils/gunicorn.py @@ -0,0 +1,33 @@ +import logging + +from gunicorn import glogging +from gunicorn.config import Config + +from snekbox import DEBUG +from .logging import FORMAT + +__all__ = ("GunicornLogger",) + + +class GunicornLogger(glogging.Logger): + """Logger for Gunicorn with custom formatting and support for the DEBUG environment variable.""" + + error_fmt = FORMAT + access_fmt = error_fmt + datefmt = None # Use the default ISO 8601 format + + def setup(self, cfg: Config) -> None: + """ + Set up loggers and set error logger's level to DEBUG if the DEBUG env var is set. + + Note: Access and syslog handlers would need to be recreated to use a custom date format + because they are created with an unspecified datefmt argument by default. + """ + super().setup(cfg) + + if DEBUG: + self.loglevel = logging.DEBUG + else: + self.loglevel = self.LOG_LEVELS.get(cfg.loglevel.lower(), logging.INFO) + + self.error_log.setLevel(self.loglevel) diff --git a/snekbox/utils/logging.py b/snekbox/utils/logging.py new file mode 100644 index 0000000..c15e3f1 --- /dev/null +++ b/snekbox/utils/logging.py @@ -0,0 +1,35 @@ +import logging +import os +import sys + +__all__ = ("FORMAT", "init_logger", "init_sentry") + +FORMAT = "%(asctime)s | %(process)5s | %(name)30s | %(levelname)8s | %(message)s" + + +def init_logger(debug: bool) -> None: + """Initialise the root logger with a handler that outputs to stdout.""" + log = logging.getLogger("snekbox") + log.setLevel(logging.DEBUG if debug else logging.INFO) + log.propagate = True + + formatter = logging.Formatter(FORMAT) + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(formatter) + log.addHandler(handler) + + +def init_sentry(version: str) -> None: + """Initialise the Sentry SDK if it's installed.""" + try: + import sentry_sdk + from sentry_sdk.integrations.falcon import FalconIntegration + except ImportError: + return + + sentry_sdk.init( + dsn=os.environ.get("SNEKBOX_SENTRY_DSN", ""), + integrations=[FalconIntegration()], + send_default_pii=True, + release=f"snekbox@{version}" + ) diff --git a/snekbox/utils/swap.py b/snekbox/utils/swap.py index 3e0d0aa..6a919cb 100644 --- a/snekbox/utils/swap.py +++ b/snekbox/utils/swap.py @@ -4,6 +4,8 @@ from pathlib import Path from snekbox.config_pb2 import NsJailConfig +__all__ = ("controller_exists", "is_enabled", "should_ignore_limit") + log = logging.getLogger(__name__) diff --git a/tests/gunicorn_utils.py b/tests/gunicorn_utils.py index f2d9b6d..b417b1b 100644 --- a/tests/gunicorn_utils.py +++ b/tests/gunicorn_utils.py @@ -17,8 +17,8 @@ class _StandaloneApplication(Application): pass def load(self): - from snekbox.api.app import application - return application + from snekbox.api import SnekAPI + return SnekAPI() def load_config(self): for key, value in self.options.items(): |