Repository: UAlbertaALTLab/morphodict (language: python)

Issue:
Host on itwewina.altlab.app

Add docker-compose config



Discussion / hints:

Fix patch:
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..5f61cc3d1
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,35 @@
+# See: https://docs.docker.com/engine/reference/builder/#dockerignore-file
+
+# Do not copy files these when dealing with Docker
+
+# Dependencies
+node_modules/
+.venv/
+
+# Logs
+CreeDictionary/DatabaseManager/logs/
+*/*.log
+
+# Caches
+.hypothesis/
+.mypy_cache
+.pytest_cache/
+
+# Databases
+**/*.sqlite3
+
+# Tests
+CreeDictionary/tests/
+cypress/
+
+# Development files
+.git/
+.vscode/
+.idea/
+.github/
+
+# When running `make debug-context`, prevent from adding itself
+context/
+
+# Do NOT inherit the .env file from the context
+/.env
diff --git a/CreeDictionary/CreeDictionary/settings.py b/CreeDictionary/CreeDictionary/settings.py
index 74db27514..b321e5dd2 100644
--- a/CreeDictionary/CreeDictionary/settings.py
+++ b/CreeDictionary/CreeDictionary/settings.py
@@ -51,9 +51,11 @@
     "django.contrib.contenttypes",
     "django.contrib.sessions",
     "django.contrib.messages",
+    # WhiteNoise nostatic HAS to come before Django's staticfiles
+    # See: http://whitenoise.evans.io/en/stable/django.html#using-whitenoise-in-development
+    "whitenoise.runserver_nostatic",
     "django.contrib.staticfiles",
     "django.contrib.humanize",
-    # Third-party apps:
     "django_js_reverse",
     # Internal apps
     # TODO: our internal app organization is kind of a mess 🙃
@@ -61,10 +63,13 @@
     "CreeDictionary.apps.CreeDictionaryConfig",
     "search_quality",
     "morphodict.apps.MorphodictConfig",
+    "DatabaseManager",
 ]
 
 MIDDLEWARE = [
     "django.middleware.security.SecurityMiddleware",
+    # Static files with WhiteNoise:
+    "whitenoise.middleware.WhiteNoiseMiddleware",
     "django.contrib.sessions.middleware.SessionMiddleware",
     "django.middleware.common.CommonMiddleware",
     "django.middleware.csrf.CsrfViewMiddleware",
@@ -298,9 +303,7 @@ def defaultDatabasePath():
 else:
     # In production, use a manifest to encourage aggressive caching
     # Note requires `python manage.py collectstatic`!
-    STATICFILES_STORAGE = (
-        "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
-    )
+    STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
 
 log_level = env.log_level("LOG_LEVEL", default="INFO")
 
diff --git a/CreeDictionary/DatabaseManager/management/commands/xmlimport.py b/CreeDictionary/DatabaseManager/management/commands/xmlimport.py
new file mode 100644
index 000000000..e5102ac42
--- /dev/null
+++ b/CreeDictionary/DatabaseManager/management/commands/xmlimport.py
@@ -0,0 +1,52 @@
+import os
+import sys
+from argparse import ArgumentParser
+from pathlib import Path
+
+from django.core.management import BaseCommand, call_command
+
+
+class Command(BaseCommand):
+    help = """Import a dictionary .xml file into db search tables.
+
+    At a high level, this will (1) analyze each wordform to get a lemma, then
+    (2) generate all wordforms of the lemmas. During this process, definitions
+    are associated with wordforms, and are also stemmed to allow searching.
+    """
+
+    def add_arguments(self, parser: ArgumentParser):
+        subparsers = parser.add_subparsers(dest="command_name")
+        subparsers.required = True
+
+        import_parser = subparsers.add_parser(
+            "import",
+            help="Import from specified crkeng.xml. This assumes the database is at migration 0001",
+        )
+        import_parser.add_argument(
+            "xml_path", help="The XML file, or directory containing crkeng*.xml"
+        )
+        import_parser.add_argument("--wipe-first", action="store_true")
+
+        subparsers.add_parser(
+            "build-test-db", help="build test_db.sqlite3 from res/test_db_words.txt"
+        )
+
+    def handle(self, *args, **options):
+        from DatabaseManager.test_db_builder import build_test_xml
+        from DatabaseManager.xml_importer import import_xmls
+        from utils import shared_res_dir
+
+        if options["command_name"] == "import":
+            if options["wipe_first"]:
+                call_command("wipedefinitions", yes_really=True)
+
+            import_xmls(Path(options["xml_path"]))
+
+        elif options["command_name"] == "build-test-db":
+            assert (
+                os.environ.get("USE_TEST_DB", "false").lower() == "true"
+            ), "Environment variable USE_TEST_DB has to be True to create test_db.sqlite3"
+            build_test_xml()
+            import_xmls(shared_res_dir / "test_dictionaries")
+        else:
+            raise NotImplementedError
diff --git a/CreeDictionary/setup.py b/CreeDictionary/setup.py
deleted file mode 100644
index 09170f980..000000000
--- a/CreeDictionary/setup.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from setuptools import find_packages, setup
-
-setup(
-    name="CreeDictionary",
-    packages=find_packages(),
-    entry_points={
-        "console_scripts": [
-            "manage-db=DatabaseManager.main:main",
-        ]
-    },
-)
diff --git a/Pipfile b/Pipfile
index 351982d44..56aff6594 100644
--- a/Pipfile
+++ b/Pipfile
@@ -13,7 +13,6 @@ pytest-mypy = "~=0.4"
 hypothesis = {extras = ["django"],version = "~=4.34"}
 codecov = "*"
 pysnooper = "*"
-tqdm = "~=4.40"
 python-levenshtein = "*"
 django-debug-toolbar = "~=2.2"
 isort = {extras = ["pyproject"],version = "*"}
@@ -26,7 +25,6 @@ coverage = "~=4.5"
 cree-sro-syllabics = ">=2020.6.23"
 pytz = "==2019.2"
 Django = "~=2.2"
-creedictionary = {path = "./CreeDictionary",editable = true} # needed to install and use database manager
 typing-extensions = "~=3.7"
 attrs = "~=19.1"
 django-js-reverse = "~=0.9"
@@ -37,6 +35,8 @@ dawg = "~=0.8"
 gunicorn = "*"
 environs = {extras = ["django"], version = "*"}
 hfst-optimized-lookup = "*"
+tqdm = "~=4.40"
+whitenoise = "*"
 
 [scripts]
 # look stuff up
diff --git a/Pipfile.lock b/Pipfile.lock
index f4122a308..1b5b2fce1 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "29331e533e268f1b251d1425605020f8c386f9e1c1b2ba9833a175a7fc0222da"
+            "sha256": "52f13c904a2f5f4f020d124803215c153d4e94cfc815d385b38dd73d01a6e9b9"
         },
         "pipfile-spec": 6,
         "requires": {
@@ -78,10 +78,6 @@
             "index": "pypi",
             "version": "==2020.6.23"
         },
-        "creedictionary": {
-            "editable": true,
-            "path": "./CreeDictionary"
-        },
         "dawg": {
             "hashes": [
                 "sha256:34881e06278d4a54cf0b402c0c8b587bef0caa78f0eee595adc7a2aa530e48ce",
@@ -109,18 +105,18 @@
         },
         "django": {
             "hashes": [
-                "sha256:558cb27930defd9a6042133258caf797b2d1dee233959f537e3dc475cb49bd7c",
-                "sha256:cf5370a4d7765a9dd6d42a7b96b53c74f9446cd38209211304b210fe0404b861"
+                "sha256:0eaca08f236bf502a9773e53623f766cc3ceee6453cc41e6de1c8b80f07d2364",
+                "sha256:c9c994f5e0a032cbd45089798b52e4080f4dea7241c58e3e0636c54146480bb4"
             ],
             "index": "pypi",
-            "version": "==2.2.17"
+            "version": "==2.2.18"
         },
         "django-cache-url": {
             "hashes": [
-                "sha256:056c115a0f590fd05e77d3b710a31f2fd531999a2b24fd8b8241bbd433e8a3b6",
-                "sha256:123c29ff93617b9ace3c8c1b6db1b5a1d42c78b62cca31c3c88045ff403b50c5"
+                "sha256:5514ca3a2075c6b956b3d0a5c540654d32b004e76340d7bdabf6661135b5f218",
+                "sha256:c1d45626ae8a206267c1263aa7a3461e2e186be2e939bcbd8c660e25851ddac8"
             ],
-            "version": "==3.2.0"
+            "version": "==3.2.3"
         },
         "django-js-reverse": {
             "hashes": [
@@ -135,11 +131,11 @@
                 "django"
             ],
             "hashes": [
-                "sha256:181fe8be5bce2512100de588b9f5ac01d8d5a867069a179efd4f3b7721036a0a",
-                "sha256:dc279947d8ce83df334031f648aa8fe4bbba12c1e3072b2eecce2ee2be8ada21"
+                "sha256:2da44b7c30114415aa858577fa6396ee326fc76a0a60f0f15e8260ba554f19dc",
+                "sha256:3f6def554abb5455141b540e6e0b72fda3853404f2b0d31658aab1bf95410db3"
             ],
             "index": "pypi",
-            "version": "==9.3.0"
+            "version": "==9.3.1"
         },
         "gunicorn": {
             "hashes": [
@@ -151,10 +147,10 @@
         },
         "hfst-optimized-lookup": {
             "hashes": [
-                "sha256:b5aacc24266a71afc7f4274978be7f2b51c807dbd495144f02aa1a2f77f1ef58"
+                "sha256:a6c9fa5329fc5d80b5f38023b37ccb1669b8805d49877bee298ad541fb8321f2"
             ],
             "index": "pypi",
-            "version": "==0.0.7"
+            "version": "==0.0.10"
         },
         "marshmallow": {
             "hashes": [
@@ -210,6 +206,14 @@
             "markers": "python_version >= '3.5'",
             "version": "==0.4.1"
         },
+        "tqdm": {
+            "hashes": [
+                "sha256:11d544652edbdfc9cc41aa4c8a5c166513e279f3f2d9f1a9e1c89935b51de6ff",
+                "sha256:a89be573bfddb81bb0b395a416d5e55e3ecc73ce95a368a4f6360bedea33195e"
+            ],
+            "index": "pypi",
+            "version": "==4.56.2"
+        },
         "typing-extensions": {
             "hashes": [
                 "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
@@ -218,6 +222,14 @@
             ],
             "index": "pypi",
             "version": "==3.7.4.3"
+        },
+        "whitenoise": {
+            "hashes": [
+                "sha256:05ce0be39ad85740a78750c86a93485c40f08ad8c62a6006de0233765996e5c7",
+                "sha256:05d00198c777028d72d8b0bbd234db605ef6d60e9410125124002518a48e515d"
+            ],
+            "index": "pypi",
+            "version": "==5.2.0"
         }
     },
     "develop": {
@@ -323,11 +335,11 @@
         },
         "django": {
             "hashes": [
-                "sha256:558cb27930defd9a6042133258caf797b2d1dee233959f537e3dc475cb49bd7c",
-                "sha256:cf5370a4d7765a9dd6d42a7b96b53c74f9446cd38209211304b210fe0404b861"
+                "sha256:0eaca08f236bf502a9773e53623f766cc3ceee6453cc41e6de1c8b80f07d2364",
+                "sha256:c9c994f5e0a032cbd45089798b52e4080f4dea7241c58e3e0636c54146480bb4"
             ],
             "index": "pypi",
-            "version": "==2.2.17"
+            "version": "==2.2.18"
         },
         "django-debug-toolbar": {
             "hashes": [
@@ -383,11 +395,11 @@
         },
         "more-itertools": {
             "hashes": [
-                "sha256:8e1a2a43b2f2727425f2b5839587ae37093f19153dc26c0927d1048ff6557330",
-                "sha256:b3a9005928e5bed54076e6e549c792b306fddfe72b2d1d22dd63d42d5d3899cf"
+                "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced",
+                "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713"
             ],
             "markers": "python_version >= '3.5'",
-            "version": "==8.6.0"
+            "version": "==8.7.0"
         },
         "mypy": {
             "hashes": [
@@ -513,10 +525,10 @@
         },
         "python-levenshtein": {
             "hashes": [
-                "sha256:554e273a88060d177e7b3c1e6ea9158dde11563bfae8f7f661f73f47e5ff0911"
+                "sha256:dc2395fbd148a1ab31090dd113c366695934b9e85fe5a4b2a032745efd0346f6"
             ],
             "index": "pypi",
-            "version": "==0.12.1"
+            "version": "==0.12.2"
         },
         "pytz": {
             "hashes": [
@@ -603,14 +615,6 @@
             "index": "pypi",
             "version": "==0.10.2"
         },
-        "tqdm": {
-            "hashes": [
-                "sha256:4621f6823bab46a9cc33d48105753ccbea671b68bab2c50a9f0be23d4065cb5a",
-                "sha256:fe3d08dd00a526850568d542ff9de9bbc2a09a791da3c334f3213d8d0bbbca65"
-            ],
-            "index": "pypi",
-            "version": "==4.56.0"
-        },
         "typed-ast": {
             "hashes": [
                 "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1",
diff --git a/docker/.gitignore b/docker/.gitignore
new file mode 100644
index 000000000..aab27cc7f
--- /dev/null
+++ b/docker/.gitignore
@@ -0,0 +1,1 @@
+nodesource.gpg.key
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 000000000..a57c0115a
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,102 @@
+FROM python:3.9-slim-buster
+
+LABEL maintainer="Eddie Antonio Santos <Eddie.Santos@nrc-cnrc.gc.ca>"
+
+# (2021-01-15): This Dockerfile was derived from Gūnáhà:
+#  - https://github.com/UAlbertaALTLab/gunaha/blob/master/Dockerfile
+#
+# Changes:
+#
+#  - use Gunicorn instead of uwsgi; I'm not sure why I chose uwsgi for Gūnáhà,
+#    but I've got itwêwina working with Gunicorn, so I'm gonna stick with that
+#
+
+
+# Directories:
+#
+# /app — the app code will live here
+#
+
+# Choose an ID that will be consistent across all machines in the network
+# To avoid overlap with user IDs, use an ID over
+# /etc/login.defs:/UID_MAX/, which defaults to 60,000
+ARG UID_GID=60003
+ARG EXPOSE_PORT=8000
+
+# Create the user/group for the application
+RUN groupadd --system --gid ${UID_GID} itwewina \
+ && useradd --no-log-init --system --gid itwewina --uid ${UID_GID} itwewina --create-home
+
+# TODO: move package.json dependences from build and test
+#  - [ ] must document in development guide
+
+# Install and build dependencies,
+# then remove build-time dependencies to keep the image slim!
+RUN set -ex \
+ && BUILD_DEPS=" \
+    build-essential \
+    tini \
+    moreutils `# for sponge` \
+    gnupg `# for verifying nodesource certificate` \
+    " \
+ && apt-get update \
+ && apt-get install -y --no-install-recommends $BUILD_DEPS \
+ && pip install pipenv \
+ && rm -rf /var/lib/apt/lists/* \
+ && mkdir /data/ /app/ \
+ && chown itwewina /data /app
+
+# Install Node + npm
+# (note: default npm is too old for the apt-install'd version of NodeJS??!?!?!)
+# Using instructions from: https://github.com/nodesource/distributions/blob/master/README.md#manual-installation
+WORKDIR /tmp
+COPY docker/nodesource.gpg.key .
+RUN set -ex \
+    && VERSION=node_12.x \
+    && DISTRO="buster" `# must be in sync with Dockerfile` \
+    && apt-key add nodesource.gpg.key \
+    && (echo "deb https://deb.nodesource.com/$VERSION $DISTRO main" | tee /etc/apt/sources.list.d/nodesource.list) \
+    && (echo "deb-src https://deb.nodesource.com/$VERSION $DISTRO main" | tee -a /etc/apt/sources.list.d/nodesource.list) \
+    && apt-get update \
+    && apt-get install -y --no-install-recommends nodejs
+
+USER itwewina
+
+WORKDIR /app/
+
+# Setup Python deps
+ADD --chown=itwewina Pipfile .
+ADD --chown=itwewina Pipfile.lock .
+
+RUN PIPENV_VENV_IN_PROJECT=1 pipenv install --deploy
+
+ADD --chown=itwewina package.json .
+ADD --chown=itwewina package-lock.json .
+# cypress is a big, heavy package that takes a long time to install and
+# that we absolutely don’t need in production
+RUN grep -Ev '"@?cypress' package.json | sponge package.json
+
+RUN npm install
+
+# Copy the application files. Make sure .dockerignore is setup properly so it
+# doesn't copy unnecessary files
+ADD --chown=itwewina . /app/
+
+RUN NODE_ENV=production npm run build \
+ && /app/.venv/bin/python CreeDictionary/manage.py collectstatic --noinput
+
+# Gunicorn will listen on this port:
+EXPOSE ${EXPOSE_PORT}
+
+# Among other things -- see
+# https://github.com/krallin/tini#tini---a-tiny-but-valid-init-for-containers
+# -- this makes typing Ctrl-C into docker-compose work
+ENTRYPOINT ["tini", "--"]
+
+WORKDIR /app/CreeDictionary
+ENV EXPOSE_PORT=${EXPOSE_PORT}
+
+CMD /app/.venv/bin/gunicorn \
+    --access-logfile - \
+    --bind 0.0.0.0:${EXPOSE_PORT} \
+    CreeDictionary.wsgi:application
diff --git a/docker/Makefile b/docker/Makefile
new file mode 100644
index 000000000..e9d35aa4f
--- /dev/null
+++ b/docker/Makefile
@@ -0,0 +1,57 @@
+# Sets up everything for running in Docker
+#
+# ## Commands ##
+#
+#  make run [default]
+#  		builds and starts the Docker container using docker-compose
+#  		creates any necessary files for building the image/running the
+#  		container
+#  make unit-file
+#  		copies the systemd unit file to the right location;
+#  		requires sudo
+#  make debug-context
+#  		runs a temporary Docker container to inspect the context
+#  		(what is copied to the Docker VM to build the image)
+
+SYSTEMD_SERVICE = docker-compose-itwewina
+
+.PHONY: run
+run: db.sqlite3 nodesource.gpg.key .env
+	docker-compose up --build
+
+.PHONY: unit-file
+unit-file: $(SYSTEMD_SERVICE).service
+	cp $< /etc/systemd/service/$(SYSTEMD_SERVICE).service
+
+.PHONY: debug-context
+debug-context:
+	printf 'FROM scratch\nCOPY . /' | DOCKER_BUILDKIT=1 docker build -f- -o context .
+
+
+############################## Concrete targets ##############################
+
+# The database file.
+# If not created before `docker-compose up` is run, then Docker will create a
+# **directory** and not an empty file ¯\_(ツ)_/¯
+#
+# Required during **container runtime**
+db.sqlite3:
+	-echo '#' Creating an empty SQLite3 database: $@
+	sqlite3 $@ 'VACUUM'
+
+# Contains configuration, like the SECRET_KEY
+#
+# N.B., the image build will create an .env to run management commands, but it
+# will be shadowed by the Docker mount command (i.e., ignored) when the
+# container is run in production.
+#
+# Required during **container runtime**
+.env:
+	-echo '#' Creating an empty .env file
+	touch $@
+
+# Key used to verify Nodesource packages on apt.
+#
+# Required during **image build**
+nodesource.gpg.key:
+	curl -sSL -O https://deb.nodesource.com/gpgkey/nodesource.gpg.key
diff --git a/docker/README.md b/docker/README.md
new file mode 100644
index 000000000..2156c69eb
--- /dev/null
+++ b/docker/README.md
@@ -0,0 +1,41 @@
+# Docker for itwêwina
+
+The current setup was created with the intent of using it in production,
+not for development.
+
+The `itwewina` container will expose **port 8001** to the machine.
+
+Please refer to the `Makefile` for the files that must be created before
+`docker-compose up --build` can be run.
+
+Because of how Docker works, these next `manage.py` commands can only work
+if the container is already running.
+
+  - Create the initial schema with:
+
+        ./docker-django-manage.py migrate
+
+  - Populate the test dictionary with
+
+        ./docker-django-manage.py xmlimport import \
+            ../CreeDictionary/res/test_dictionaries/crkeng.xml
+
+## Deployment on `altlab-itw`
+
+We created a **system user** called `itwewina`, which is part of the
+`itwewina` and `docker` groups. The user's home directory is
+`/opt/docker-compose/itwewina` and the repo is cloned there.
+
+On `altlab-itw`, you will need to copy over the `systemd` unit file:
+
+    sudo make unit-file
+
+And then enable the service:
+
+    sudo systemctl daemon-reload
+    sudo systemctl enable docker-compose-itwewina
+
+## Future work
+
+  - smaller Docker image without dev dependencies through multi-stage
+    builds
diff --git a/docker/docker-compose-itwewina.service b/docker/docker-compose-itwewina.service
new file mode 100644
index 000000000..4deb1b41a
--- /dev/null
+++ b/docker/docker-compose-itwewina.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=itwewina service with docker-compose
+
+[Service]
+User=itwewina
+Type=oneshot
+RemainAfterExit=true
+WorkingDirectory=/opt/docker-compose/itwewina/cree-intelligent-dictionary/docker
+ExecStart=/usr/bin/docker-compose up -d --remove-orphans
+ExecStop=/usr/bin/docker-compose down
+
+[Install]
+WantedBy=multi-user.target
\ No newline at end of file
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
new file mode 100644
index 000000000..189be7495
--- /dev/null
+++ b/docker/docker-compose.yml
@@ -0,0 +1,12 @@
+version: "3"
+services:
+  app:
+    build:
+      context: ..
+      dockerfile: docker/Dockerfile
+    volumes:
+      - "./db.sqlite3:/app/CreeDictionary/db.sqlite3"
+      - "./.env:/app/.env"
+    ports:
+      # gunaha was using 8000, so let's use 8001, yay!
+      - 8001:8000
diff --git a/docker/docker-django-manage.py b/docker/docker-django-manage.py
new file mode 100755
index 000000000..296c8df75
--- /dev/null
+++ b/docker/docker-django-manage.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+
+# Fun fact! This started as a shell script but the .py extension confused black.
+import os
+import sys
+from pathlib import Path
+from shutil import which
+
+dir = Path(__file__).parent
+
+docker_compose = which("docker-compose")
+
+os.execv(
+    docker_compose,
+    [
+        docker_compose,
+        # By passing the full path to the yml, the current wrapper script
+        # can be called from any working directory
+        "-f",
+        dir.absolute() / "docker-compose.yml",
+        "exec",
+        "app",
+        "/app/.venv/bin/python",
+        "./manage.py",
+    ]
+    + sys.argv[1:],
+)
diff --git a/docker/run-shell b/docker/run-shell
new file mode 100755
index 000000000..c0f707335
--- /dev/null
+++ b/docker/run-shell
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+DIR="$(dirname -- "${0}")"
+
+exec docker-compose -f "${DIR}/docker-compose.yml" exec app \
+        /bin/bash
+


Test patch:
diff --git a/CreeDictionary/tests/DatabaseManager_tests/cmd_test.py b/CreeDictionary/tests/DatabaseManager_tests/cmd_test.py
index 20224ea00..c8ebe643a 100644
--- a/CreeDictionary/tests/DatabaseManager_tests/cmd_test.py
+++ b/CreeDictionary/tests/DatabaseManager_tests/cmd_test.py
@@ -1,14 +1,15 @@
 import pytest
+from django.core.management import call_command
+
 from API.models import Wordform
-from DatabaseManager.main import main
 from DatabaseManager.cree_inflection_generator import expand_inflections
-from django.core.management import call_command
+from DatabaseManager.xml_importer import import_xmls
 
 
 @pytest.mark.django_db
 def test_import_nice_xml(shared_datadir):
     call_command("migrate", "API", "0001")
-    main([..., "import", str(shared_datadir / "crkeng-small-nice-0")])
+    import_xmls(shared_datadir / "crkeng-small-nice-0")
 
     expanded = expand_inflections(["yôwamêw+V+TA+Ind+3Sg+4Sg/PlO"], verbose=False)
     for analysis_and_inflections in expanded.values():