Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

24 changed files with 601 additions and 869 deletions

View File

@ -1,13 +0,0 @@
# SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
*
!Pipfile
!Pipfile.lock
!data/
!ki/
!LICENSES/
!migrations/
!app.py
!run_prod.py

View File

@ -4,125 +4,48 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
kind: pipeline
type: docker
name: qa
trigger:
event:
- push
- pull_request
branch:
- main
name: default
steps:
- name: install-lint-test
image: python:3.8.20-alpine@sha256:3d93b1f77efce339aa77db726656872517b0d67837989aa7c4b35bd5ae7e81ba
env:
PYROOT: '/pyroot'
PYTHONUSERBASE: '/pyroot'
- name: qa
image: registry.wtf-eg.net/ki-backend-builder:1.0.0
commands:
- apk add --no-cache gcc g++ musl-dev python3-dev
- pip3 install pipenv
- pipenv verify
- pipenv install --dev
- pipenv run flake8
- pipenv run reuse lint
- pipenv run python -m unittest discover ki
- name: docker-dry-run
image: plugins/docker:20.18.4@sha256:a8d3d86853c721492213264815f1d00d3ed13f42f5c1855a02f47fa4d5f1e042
settings:
registry: git.wtf-eg.de
repo: git.wtf-eg.de/kompetenzinventar/backend
target: ki-backend
dry_run: true
when:
event:
- pull_request
---
kind: pipeline
type: docker
name: build
trigger:
event:
- push
branch:
- main
depends_on:
- qa
steps:
- name: docker-publish
image: plugins/docker:20.18.4@sha256:a8d3d86853c721492213264815f1d00d3ed13f42f5c1855a02f47fa4d5f1e042
image: plugins/docker
settings:
registry: git.wtf-eg.de
repo: git.wtf-eg.de/kompetenzinventar/backend
registry: registry.wtf-eg.net
repo: registry.wtf-eg.net/ki-backend
target: ki-backend
auto_tag: true
username:
from_secret: "docker_username"
password:
from_secret: "docker_password"
---
kind: pipeline
type: docker
name: deploy
trigger:
when:
event:
- push
branch:
- main
depends_on:
- build
steps:
- name: deploy-dev
image: appleboy/drone-ssh:1.7.5@sha256:995677e073454912f26d4c0fdd2f9df2e1f5a30d6603d3f2ece667311b6babb3
- name: docker-publish-tag
image: plugins/docker
settings:
host:
- dev01.wtf-eg.net
username: drone_deployment
key:
from_secret: "dev01_deployment_key"
command_timeout: 2m
script:
- echo "Executing forced command..."
---
kind: pipeline
type: docker
name: tag-release
trigger:
registry: registry.wtf-eg.net
repo: registry.wtf-eg.net/ki-backend
target: ki-backend
auto_tag: true
username:
from_secret: "docker_username"
password:
from_secret: "docker_password"
when:
event:
- tag
steps:
- name: install-lint-test
image: python:3.8.20-alpine@sha256:3d93b1f77efce339aa77db726656872517b0d67837989aa7c4b35bd5ae7e81ba
env:
PYROOT: '/pyroot'
PYTHONUSERBASE: '/pyroot'
commands:
- apk add --no-cache gcc g++ musl-dev python3-dev
- pip3 install pipenv
- pipenv install --dev
- pipenv run flake8
- pipenv run reuse lint
- pipenv run python -m unittest discover ki
- name: docker-publish
image: plugins/docker:20.18.4@sha256:a8d3d86853c721492213264815f1d00d3ed13f42f5c1855a02f47fa4d5f1e042
settings:
registry: git.wtf-eg.de
repo: git.wtf-eg.de/kompetenzinventar/backend
target: ki-backend
auto_tag: true
username:
from_secret: "docker_username"
password:
from_secret: "docker_password"
image_pull_secrets:
- dockerconfig

View File

@ -1 +0,0 @@
3.8.20

12
.reuse/dep5 Normal file
View File

@ -0,0 +1,12 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: Kompetenzinventar
Upstream-Contact: Michael Weimann <mail@michael-weimann.eu>
Source: https://git.wtf-eg.de/kompetenzinventar/ki-backend
Files: data/imgs/flags/*
Copyright: 2013 Panayiotis Lipiridis <https://flagicons.lipis.dev/>
License: MIT
Files: Pipfile.lock migrations/*
Copyright: WTF Kooperative eG <https://wtf-eg.de/>
License: AGPL-3.0-or-later

View File

@ -1,5 +0,0 @@
# SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
migrations/*.py

View File

@ -2,17 +2,7 @@
#
# SPDX-License-Identifier: AGPL-3.0-or-later
FROM python:3.8.20-alpine@sha256:3d93b1f77efce339aa77db726656872517b0d67837989aa7c4b35bd5ae7e81ba AS builder
ENV PYROOT=/pyroot
ENV PYTHONUSERBASE=$PYROOT
RUN apk add --no-cache \
gcc \
g++ \
musl-dev \
python3-dev && \
pip3 install pipenv
FROM registry.wtf-eg.net/ki-backend-builder:1.0.0 as builder
COPY Pipfile* ./
@ -20,10 +10,7 @@ RUN PIP_USER=1 PIP_IGNORE_INSTALLED=1 pipenv install --system --deploy --ignore-
RUN pip3 uninstall --yes pipenv
FROM python:3.8.20-alpine@sha256:3d93b1f77efce339aa77db726656872517b0d67837989aa7c4b35bd5ae7e81ba AS ki-backend
ENV PYROOT=/pyroot
ENV PYTHONUSERBASE=$PYROOT
FROM registry.wtf-eg.net/ki-backend-base:1.0.0 as ki-backend
# Install six explicitly. Otherwise Python complains about it missing.
RUN pip3 install six
@ -35,9 +22,4 @@ WORKDIR /app
COPY . .
LABEL org.opencontainers.image.source=https://git.wtf-eg.de/kompetenzinventar/ki-backend.git
LABEL org.opencontainers.image.url=https://git.wtf-eg.de/kompetenzinventar/ki-backend
LABEL org.opencontainers.image.documentation=https://git.wtf-eg.de/kompetenzinventar/ki-backend#docker
LABEL org.opencontainers.image.vendor="WTF Kooperative eG"
CMD ["python3", "run_prod.py"]

29
Pipfile
View File

@ -8,23 +8,22 @@ verify_ssl = true
name = "pypi"
[packages]
flask = "==2.3.3"
python-dotenv = "==1.0.1"
flask-migrate = "==4.0.7"
flask-sqlalchemy = "==2.5.1"
sqlalchemy = "==1.4.54"
waitress = "==2.1.2"
pyyaml = "==6.0.2"
flask-cors = "==5.0.0"
ldap3 = "==2.9.1"
pymysql = "==1.1.1"
werkzeug = "==2.3.8"
flask = "~=2.0.1"
python-dotenv = "~=0.17.1"
flask-migrate = "~=3.0.1"
flask-sqlalchemy = "~=2.5.1"
sqlalchemy = "~=1.4.18"
waitress = "~=2.0.0"
pyyaml = "~=5.4.1"
flask-cors = "~=3.0.10"
ldap3 = "~=2.9"
pymysql = "~=1.0.2"
[dev-packages]
flake8 = "==7.1.1"
yapf = "==0.40.2"
pre-commit = "==2.21.0"
reuse = "==4.0.3"
flake8 = "~=3.9.2"
yapf = "~=0.31.0"
pre-commit = "~=2.13.0"
reuse = "~=0.13.0"
[requires]
python_version = "3.8"

959
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -222,6 +222,25 @@ docker-compose up
Dann http://localhost:13337 aufrufen.
### Workaround, falls der Zugriff auf registry.wtf-eg.net nicht möglich ist
Voraussetzung:
[ki-backend-docker](https://git.wtf-eg.de/kompetenzinventar/ki-backend-docker) muss parallel zum `ki-backend` ausgecheckt sein.
```
cd ki-backend-docker
docker build . --target base -t ki-backend-base
docker build . --target builder -t ki-backend-builder
```
Ändern der 2 Einträge im `Dockerfile` des `ki-backend`:
- registry.wtf-eg.net/ki-backend-builder:1.0.0 -> ki-backend-builder
- registry.wtf-eg.net/ki-backend-base:1.0.0 -> ki-backend-base
Danach sollte `docker-compose up` funktionieren.
## Lizenzen
Dieses Projekt erfüllt die [REUSE](https://reuse.software/) Spezifikation.

View File

@ -1,24 +0,0 @@
# SPDX-FileCopyrightText: NONE
# SPDX-License-Identifier: CC0-1.0
version = 1
SPDX-PackageName = "Kompetenzinventar Backend"
SPDX-PackageDownloadLocation = "https://git.wtf-eg.de/kompetenzinventar/ki-backend"
[[annotations]]
path = "data/imgs/flags/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2013 Panayiotis Lipiridis <https://flagicons.lipis.dev/>"
SPDX-License-Identifier = "MIT"
[[annotations]]
path = ["Pipfile.lock", "migrations/**"]
precedence = "aggregate"
SPDX-FileCopyrightText = "WTF Kooperative eG <https://wtf-eg.de/>"
SPDX-License-Identifier = "AGPL-3.0-or-later"
[[annotations]]
path = ["renovate.json", ".python-version"]
precedence = "aggregate"
SPDX-FileCopyrightText = "WTF Kooperative eG <https://wtf-eg.de/>"
SPDX-License-Identifier = "AGPL-3.0-or-later"

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="810" height="540">
<rect width="810" height="540" fill="#FCDD09"/>
<path stroke="#DA121A" stroke-width="60" d="M0,90H810m0,120H0m0,120H810m0,120H0"/>
</svg>

Before

Width:  |  Height:  |  Size: 242 B

View File

@ -25,30 +25,22 @@ def seed_contacttypes():
db.session.add(ContactType(id=int(contacttype["id"]), name=contacttype["name"]))
def seed_user(auth_id,
nickname=None,
pronouns="",
visible=True,
def seed_user(nickname,
visible=False,
skills=[],
languages=[],
volunteerwork="",
availability_status=False,
freetext="",
availability_text="",
availability_hours_per_week=42,
skills=[],
searchtopics=[],
languages=[],
address=None,
contacts=[]):
if not nickname:
nickname = auth_id
availability_hours_per_week=42):
app.logger.info(f"seeding {nickname} \\o/")
user = User(auth_id=auth_id)
user = User(auth_id=nickname)
db.session.add(user)
profile = Profile(nickname=nickname,
pronouns=pronouns,
pronouns="",
volunteerwork=volunteerwork,
availability_status=availability_status,
availability_text=availability_text,
@ -61,29 +53,10 @@ def seed_user(auth_id,
skill = ProfileSkill(profile=profile, skill_id=skill_data[0], level=skill_data[1])
db.session.add(skill)
for skill_id in searchtopics:
searchtopic = ProfileSearchtopic(profile=profile, skill_id=skill_id)
db.session.add(searchtopic)
for language_data in languages:
language = ProfileLanguage(profile=profile, language_id=language_data[0], level=language_data[1])
db.session.add(language)
if address:
_address = Address(name=address[0],
street=address[1],
house_number=address[2],
additional=address[3],
postcode=address[4],
city=address[5],
country=address[6],
profile=profile)
db.session.add(_address)
for contact_data in contacts:
contact = Contact(profile=profile, contacttype_id=contact_data[0], content=contact_data[1])
db.session.add(contact)
db.session.add(profile)
@ -119,40 +92,77 @@ def seed(dev: bool):
db.session.add(Language(id=iso["639-1"], name=iso["Sprache"]))
if dev:
seed_user("klaus", visible=False)
app.logger.info("seeding peter :)")
for i in range(1, 20):
seed_user(f"babsi{i}")
peter = User(auth_id="peter")
db.session.add(peter)
seed_user("peter",
nickname="peternichtlustig",
visible=False,
peters_profile = Profile(nickname="peternichtlustig",
pronouns="Herr Dr. Dr.",
volunteerwork="Gartenverein",
availability_status=True,
availability_hours_per_week=42,
availability_text="Immer",
freetext="Ich mag Kaffee",
skills=[(3, 3), (1, 5)],
searchtopics=[3, 1],
languages=[("de", 5), ("fr", 3)],
address=("Peter Nichtlustig", "Waldweg", "23i", "Hinterhaus", "13337", "Bielefeld", "Deutschland"),
contacts=[(4, "@peter:wtf-eg.de"), (1, "peter@wtf-eg.de")])
user=peter)
db.session.add(peters_profile)
matrix_contact = Contact(profile=peters_profile, contacttype_id=4, content="@peter:wtf-eg.de")
db.session.add(matrix_contact)
email_contact = Contact(profile=peters_profile, contacttype_id=1, content="peter@wtf-eg.de")
db.session.add(email_contact)
peters_address = Address(name="Peter Nichtlustig",
street="Waldweg",
house_number="23i",
additional="Hinterhaus",
postcode="13337",
city="Bielefeld",
country="Deutschland",
profile=peters_profile)
db.session.add(peters_address)
peters_python_skill = ProfileSkill(profile=peters_profile, skill_id=3, level=3)
db.session.add(peters_python_skill)
peters_php_skill = ProfileSkill(profile=peters_profile, skill_id=1, level=5)
db.session.add(peters_php_skill)
peters_python_searchtopic = ProfileSearchtopic(profile=peters_profile, skill_id=3)
db.session.add(peters_python_searchtopic)
peters_php_searchtopic = ProfileSearchtopic(profile=peters_profile, skill_id=1)
db.session.add(peters_php_searchtopic)
peter_de = ProfileLanguage(profile=peters_profile, language_id="de", level=5)
db.session.add(peter_de)
peter_fr = ProfileLanguage(profile=peters_profile, language_id="fr", level=3)
db.session.add(peter_fr)
seed_user("klaus")
for i in range(1, 20):
seed_user(f"babsi{i}", visible=True)
seed_user("dirtydieter",
visible=True,
volunteerwork="Müll sammeln",
availability_status=True,
availability_hours_per_week=24,
availability_text="Nur Nachts!",
freetext="1001010010111!!!",
skills=[(1, 5)],
address=("Friedrich Witzig", "", "", "", "", "", ""))
skills=[(Skill.skill_id_php, 5)])
all_skills = [(skill.id, 3) for skill in Skill.query.all()]
seed_user("jutta", languages=[("fr", 5)], skills=all_skills)
all_skills = Skill.query.all()
all_profile_skills = []
for skill in all_skills:
all_profile_skills.append((skill.id, 3))
seed_user("giesela", skills=[(9, 3), (10, 5)])
seed_user("bertha", visible=False, skills=[(11, 3), (10, 5)])
seed_user("monique", languages=[("fr", 4)])
seed_user("jutta", visible=True, languages=[("fr", 5)], skills=all_profile_skills)
seed_user("giesela", visible=True, skills=[(Skill.skill_id_mysql, 3), (Skill.skill_id_postgresql, 5)])
seed_user("bertha", visible=False, skills=[(Skill.skill_id_sqlite, 3), (Skill.skill_id_postgresql, 5)])
seed_user("monique", visible=True, languages=[("fr", 4)])
db.session.commit()

View File

@ -4,7 +4,7 @@
from flask import make_response, request
from ki.models import Profile, ProfileSkill, Skill, ProfileLanguage, Language, Address
from ki.models import Profile, ProfileSkill, Skill, ProfileLanguage, Language
def find_profiles():
@ -25,18 +25,13 @@ def find_profiles():
.order_by(Profile.nickname) \
.filter(Profile.visible.is_(True)) \
.join(Profile.skills, isouter=True).join(ProfileSkill.skill, isouter=True) \
.join(Profile.languages, isouter=True).join(ProfileLanguage.language, isouter=True) \
.join(Address, isouter=True)
.join(Profile.languages, isouter=True).join(ProfileLanguage.language, isouter=True)
if "search" in request.args:
terms = request.args["search"].split(" ")
for term in terms:
query = query.filter(
Profile.nickname.like(f"%{term}%") | # noqa: W504
Skill.name.like(f"%{term}%") | # noqa: W504
Language.name.like(f"%{term}%") | # noqa: W504
Address.name.like(f"%{term}%"))
Profile.nickname.like(f"%{term}%") | Skill.name.like(f"%{term}%") | Language.name.like(f"%{term}%"))
if "nickname" in request.args:
nickname = request.args.get("nickname")

View File

@ -151,11 +151,11 @@ def update_profile(user_id: int):
profile.freetext = request.json.get("freetext", "")
profile.visible = request.json.get("visible", False)
update_address(profile, request.json.get("address"))
update_contacts(profile, request.json.get("contacts", []))
update_skills(profile, request.json.get("skills", []))
update_searchtopics(profile, request.json.get("searchtopics", []))
update_languages(profile, request.json.get("languages", []))
update_address(profile, request.json.get("address", {}))
update_contacts(profile, request.json.get("contacts", {}))
update_skills(profile, request.json.get("skills", {}))
update_searchtopics(profile, request.json.get("searchtopics"))
update_languages(profile, request.json.get("languages", {}))
db.session.commit()

View File

@ -139,6 +139,13 @@ class Address(db.Model):
class Skill(db.Model):
skill_id_php = 1
skill_id_python = 3
skill_id_sqlalchemy = 7
skill_id_mysql = 9
skill_id_postgresql = 10
skill_id_sqlite = 11
__tablename__ = "skill"
id = Column(Integer, primary_key=True)

View File

@ -17,7 +17,6 @@ content_type_png = "image/png"
def token_auth(func):
@wraps(func)
def _token_auth(*args, **kwargs):
auth_header = request.headers.get("Authorization")

View File

@ -8,7 +8,6 @@ from ki.test.ApiTest import ApiTest
class TestContactTypesEndpoint(ApiTest):
def test_skills_options(self):
response = self.client.options("/contacttypes")
self.assertEqual(response.status_code, 200)

View File

@ -8,7 +8,6 @@ from ki.test.ApiTest import ApiTest
class TestFindProfilesEndpoint(ApiTest):
def test_find_profiles_options(self):
response = self.client.options("/users/profiles")
self.assertEqual(response.status_code, 200)
@ -100,14 +99,6 @@ class TestFindProfilesEndpoint(ApiTest):
self.assertDictContainsSubset({"nickname": "jutta"}, response.json["profiles"][0])
self.assertDictContainsSubset({"nickname": "monique"}, response.json["profiles"][1])
def test_find_dieter_by_name(self):
token = self.login("peter", "geheim")["token"]
response = self.client.get("/users/profiles?search=friedrich", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset({"total": 1}, response.json)
self.assertDictContainsSubset({"nickname": "dirtydieter"}, response.json["profiles"][0])
if __name__ == "main":
unittest.main()

View File

@ -8,7 +8,6 @@ from ki.test.ApiTest import ApiTest
class TestLanguagesEndpoint(ApiTest):
def test_skills_options(self):
response = self.client.options("/languages")
self.assertEqual(response.status_code, 200)

View File

@ -10,7 +10,6 @@ from ki.test.ApiTest import ApiTest
class TestLoginEndpoint(ApiTest):
def test_login(self):
response1_data = self.login("peter", "geheim")
response2_data = self.login("peter", "geheim")

View File

@ -20,8 +20,7 @@ class TestProfileEndpoint(ApiTest):
self.assertEqual(login_response.status_code, 200)
self.assertIn("token", login_response.json)
babsi = User.query.filter(User.auth_id == "babsi1").first()
response = self.client.post(f"/users/{babsi.id}/profile",
response = self.client.post("/users/1/profile",
data=json.dumps({}),
content_type="application/json",
headers={"Authorization": "Bearer " + login_response.json["token"]})
@ -103,15 +102,14 @@ class TestProfileEndpoint(ApiTest):
"level": 2
}]
}
peter = User.query.filter(User.auth_id == "peter").first()
response = self.client.post(f"/users/{peter.id}/profile",
response = self.client.post("/users/1/profile",
data=json.dumps(data),
content_type="application/json",
headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
with app.app_context():
user = User.query.filter(User.id == peter.id).first()
user = User.query.filter(User.id == 1).first()
profile = user.profile
self.assertEqual("Hebbert", profile.nickname)
self.assertEqual("Monsieur", profile.pronouns)
@ -185,8 +183,7 @@ class TestProfileEndpoint(ApiTest):
def test_get_visible_proifle(self):
token = self.login("peter", "geheim")["token"]
babsi = User.query.filter(User.auth_id == "babsi1").first()
response = self.client.get(f"/users/{babsi.id}/profile", headers={"Authorization": f"Bearer {token}"})
response = self.client.get("/users/3/profile", headers={"Authorization": f"Bearer {token}"})
self.assertEqual(response.status_code, 200)
@ -197,15 +194,14 @@ class TestProfileEndpoint(ApiTest):
self.assertEqual(login_response.status_code, 200)
self.assertIn("token", login_response.json)
peter = User.query.filter(User.auth_id == "peter").first()
response = self.client.get(f"/users/{peter.id}/profile",
response = self.client.get("/users/1/profile",
headers={"Authorization": "Bearer " + login_response.json["token"]})
profile_id = peter.profile.id
self.assertEqual(response.status_code, 200)
self.assertDictEqual(
response.json, {
"profile": {
"user_id": peter.id,
"user_id": 1,
"nickname": "peternichtlustig",
"pronouns": "Herr Dr. Dr.",
"availability_status": True,
@ -222,12 +218,12 @@ class TestProfileEndpoint(ApiTest):
"id": 1,
"name": "Peter Nichtlustig",
"postcode": "13337",
"profile_id": profile_id,
"profile_id": 1,
"street": "Waldweg"
},
"contacts": [{
"id": 1,
"profile_id": profile_id,
"profile_id": 1,
"contacttype": {
"id": 4,
"name": "Matrix"
@ -235,7 +231,7 @@ class TestProfileEndpoint(ApiTest):
"content": "@peter:wtf-eg.de"
}, {
"id": 2,
"profile_id": profile_id,
"profile_id": 1,
"contacttype": {
"id": 1,
"name": "E-Mail"
@ -243,7 +239,7 @@ class TestProfileEndpoint(ApiTest):
"content": "peter@wtf-eg.de"
}],
"skills": [{
"profile_id": profile_id,
"profile_id": 1,
"skill": {
"id": 1,
"name": "PHP",
@ -251,7 +247,7 @@ class TestProfileEndpoint(ApiTest):
},
"level": 5
}, {
"profile_id": profile_id,
"profile_id": 1,
"skill": {
"id": 3,
"name": "Python",
@ -260,14 +256,14 @@ class TestProfileEndpoint(ApiTest):
"level": 3
}],
"searchtopics": [{
"profile_id": profile_id,
"profile_id": 1,
"skill": {
"id": 1,
"name": "PHP",
"icon_url": "/skills/1/icon"
}
}, {
"profile_id": profile_id,
"profile_id": 1,
"skill": {
"id": 3,
"name": "Python",
@ -275,7 +271,7 @@ class TestProfileEndpoint(ApiTest):
}
}],
"languages": [{
"profile_id": profile_id,
"profile_id": 1,
"language": {
"id": "de",
"name": "Deutsch",
@ -283,7 +279,7 @@ class TestProfileEndpoint(ApiTest):
},
"level": 5
}, {
"profile_id": profile_id,
"profile_id": 1,
"language": {
"id": "fr",
"name": "Französisch",

View File

@ -8,7 +8,6 @@ from ki.test.ApiTest import ApiTest
class TestSkillsEndpoint(ApiTest):
def test_skills_options(self):
response = self.client.options("/skills")
self.assertEqual(response.status_code, 200)

View File

@ -1,18 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:best-practices",
":disableDependencyDashboard",
":maintainLockFilesMonthly",
":pinVersions",
":separateMultipleMajorReleases"
],
"packageRules": [
{
"matchDepNames": ["python"],
"groupName": "Python",
"separateMinorPatch": true,
"separateMultipleMinor": true
}
]
}

View File

@ -11,4 +11,4 @@ with app.app_context():
config = migrate.get_config()
command.upgrade(config, "head")
serve(app, host="0.0.0.0", port=5000, threads=20)
serve(app, host="0.0.0.0", port=5000)