Compare commits

..

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

21 changed files with 610 additions and 522 deletions

View File

@ -15,28 +15,17 @@ trigger:
steps: steps:
- name: install-lint-test - name: install-lint-test
image: python:3.9.21-alpine@sha256:f2f6a5627a879693b8c23e04df0b1a6aae3e09c165fa2a08f5c64b2b54c58d3c image: python:3.8.19-alpine@sha256:3bd7ea88cb637e09d6c7de24c5394657163a85c2be82bfebe0305cf07f8de1ea
env: env:
PYROOT: '/pyroot' PYROOT: '/pyroot'
PYTHONUSERBASE: '/pyroot' PYTHONUSERBASE: '/pyroot'
commands: commands:
- apk add --no-cache gcc g++ musl-dev python3-dev - apk add --no-cache gcc g++ musl-dev python3-dev
- pip3 install pipenv - pip3 install pipenv
- pipenv verify
- pipenv install --dev - pipenv install --dev
- pipenv run flake8 - pipenv run flake8
- pipenv run reuse lint - pipenv run reuse lint
- SQLALCHEMY_DATABASE_URI=sqlite:// pipenv run python -m unittest discover ki - pipenv run python -m unittest discover ki
- name: docker-dry-run
image: plugins/docker:20.18.6@sha256:59c993e3c4e6c097a0e2d274419aac0d7d8e929773f0ba1af44078e54389834f
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 kind: pipeline
@ -54,7 +43,7 @@ depends_on:
steps: steps:
- name: docker-publish - name: docker-publish
image: plugins/docker:20.18.6@sha256:59c993e3c4e6c097a0e2d274419aac0d7d8e929773f0ba1af44078e54389834f image: plugins/docker:20.18.4@sha256:a8d3d86853c721492213264815f1d00d3ed13f42f5c1855a02f47fa4d5f1e042
settings: settings:
registry: git.wtf-eg.de registry: git.wtf-eg.de
repo: git.wtf-eg.de/kompetenzinventar/backend repo: git.wtf-eg.de/kompetenzinventar/backend
@ -104,7 +93,7 @@ trigger:
steps: steps:
- name: install-lint-test - name: install-lint-test
image: python:3.9.21-alpine@sha256:f2f6a5627a879693b8c23e04df0b1a6aae3e09c165fa2a08f5c64b2b54c58d3c image: python:3.8.19-alpine@sha256:3bd7ea88cb637e09d6c7de24c5394657163a85c2be82bfebe0305cf07f8de1ea
env: env:
PYROOT: '/pyroot' PYROOT: '/pyroot'
PYTHONUSERBASE: '/pyroot' PYTHONUSERBASE: '/pyroot'
@ -114,9 +103,9 @@ steps:
- pipenv install --dev - pipenv install --dev
- pipenv run flake8 - pipenv run flake8
- pipenv run reuse lint - pipenv run reuse lint
- SQLALCHEMY_DATABASE_URI=sqlite:// pipenv run python -m unittest discover ki - pipenv run python -m unittest discover ki
- name: docker-publish - name: docker-publish
image: plugins/docker:20.18.6@sha256:59c993e3c4e6c097a0e2d274419aac0d7d8e929773f0ba1af44078e54389834f image: plugins/docker:20.18.4@sha256:a8d3d86853c721492213264815f1d00d3ed13f42f5c1855a02f47fa4d5f1e042
settings: settings:
registry: git.wtf-eg.de registry: git.wtf-eg.de
repo: git.wtf-eg.de/kompetenzinventar/backend repo: git.wtf-eg.de/kompetenzinventar/backend

View File

@ -1 +1 @@
3.9.21 3.8.19

16
.reuse/dep5 Normal file
View File

@ -0,0 +1,16 @@
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
Files: renovate.json .python-version
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,7 +2,7 @@
# #
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
FROM python:3.9.21-alpine@sha256:f2f6a5627a879693b8c23e04df0b1a6aae3e09c165fa2a08f5c64b2b54c58d3c AS builder FROM python:3.8.19-alpine@sha256:3bd7ea88cb637e09d6c7de24c5394657163a85c2be82bfebe0305cf07f8de1ea AS builder
ENV PYROOT=/pyroot ENV PYROOT=/pyroot
ENV PYTHONUSERBASE=$PYROOT ENV PYTHONUSERBASE=$PYROOT
@ -20,7 +20,7 @@ RUN PIP_USER=1 PIP_IGNORE_INSTALLED=1 pipenv install --system --deploy --ignore-
RUN pip3 uninstall --yes pipenv RUN pip3 uninstall --yes pipenv
FROM python:3.9.21-alpine@sha256:f2f6a5627a879693b8c23e04df0b1a6aae3e09c165fa2a08f5c64b2b54c58d3c AS ki-backend FROM python:3.8.19-alpine@sha256:3bd7ea88cb637e09d6c7de24c5394657163a85c2be82bfebe0305cf07f8de1ea AS ki-backend
ENV PYROOT=/pyroot ENV PYROOT=/pyroot
ENV PYTHONUSERBASE=$PYROOT ENV PYTHONUSERBASE=$PYROOT

24
Pipfile
View File

@ -8,26 +8,26 @@ verify_ssl = true
name = "pypi" name = "pypi"
[packages] [packages]
flask = "==2.3.3" flask = "==2.0.3"
python-dotenv = "==1.0.1" python-dotenv = "==0.17.1"
flask-migrate = "==4.0.7" flask-migrate = "==3.0.1"
flask-sqlalchemy = "==3.1.1" flask-sqlalchemy = "==2.5.1"
sqlalchemy = "==2.0.36" sqlalchemy = "==1.4.53"
waitress = "==2.1.2" waitress = "==2.1.2"
pyyaml = "==6.0.2" pyyaml = "==6.0.2"
flask-cors = "==5.0.0" flask-cors = "==3.0.10"
ldap3 = "==2.9.1" ldap3 = "==2.9.1"
pymysql = "==1.1.1" pymysql = "==1.0.3"
werkzeug = "==2.3.8" werkzeug = "==2.3.8"
[dev-packages] [dev-packages]
flake8 = "==7.1.1" flake8 = "==3.9.2"
yapf = "==0.40.2" yapf = "==0.31.0"
pre-commit = "==2.21.0" pre-commit = "==2.13.0"
reuse = "==4.0.3" reuse = "==0.14.0"
[requires] [requires]
python_version = "3.9" python_version = "3.8"
[scripts] [scripts]
clean = "rm data/ki.sqlite" clean = "rm data/ki.sqlite"

854
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -39,7 +39,7 @@ Folgende Kanäle gibt es für die Kommunikation über das Kompetenzinventar:
### Abhängigkeiten ### Abhängigkeiten
- Python 3.9 - Python 3.8
- [Pipenv](https://github.com/pypa/pipenv) - [Pipenv](https://github.com/pypa/pipenv)

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"

2
app.py
View File

@ -38,7 +38,7 @@ app.config["KI_LDAP_AUTH_PASSWORD"] = os.getenv("KI_LDAP_AUTH_PASSWORD")
app.config["KI_LDAP_BASE_DN"] = os.getenv("KI_LDAP_BASE_DN") app.config["KI_LDAP_BASE_DN"] = os.getenv("KI_LDAP_BASE_DN")
CORS(app) CORS(app)
db = SQLAlchemy(app, session_options={"future": True}) db = SQLAlchemy(app)
migrate = Migrate(app, db, compare_type=True) migrate = Migrate(app, db, compare_type=True)
app.logger.info("Hello from KI") app.logger.info("Hello from KI")

View File

@ -19,7 +19,7 @@ def seed_contacttypes():
for contacttype in csv_reader: for contacttype in csv_reader:
id = int(contacttype["id"]) id = int(contacttype["id"])
db_contacttype = db.session.get(ContactType, id) db_contacttype = ContactType.query.get(id)
if db_contacttype is None: if db_contacttype is None:
db.session.add(ContactType(id=int(contacttype["id"]), name=contacttype["name"])) db.session.add(ContactType(id=int(contacttype["id"]), name=contacttype["name"]))
@ -88,7 +88,6 @@ def seed_user(auth_id,
def seed(dev: bool): def seed(dev: bool):
with app.app_context():
seed_contacttypes() seed_contacttypes()
skill_seed_file_path = app.config["KI_DATA_DIR"] + "/seed_data/skills.csv" skill_seed_file_path = app.config["KI_DATA_DIR"] + "/seed_data/skills.csv"
@ -100,7 +99,7 @@ def seed(dev: bool):
for skill in skills_csv_reader: for skill in skills_csv_reader:
id = int(skill["id"]) id = int(skill["id"])
db_skill = db.session.get(Skill, id) db_skill = Skill.query.get(id)
if db_skill is None: if db_skill is None:
db.session.add(Skill(id=int(skill["id"]), name=skill["name"])) db.session.add(Skill(id=int(skill["id"]), name=skill["name"]))
@ -114,7 +113,7 @@ def seed(dev: bool):
for iso in iso_csv_reader: for iso in iso_csv_reader:
id = iso["639-1"] id = iso["639-1"]
db_language = db.session.get(Language, id) db_language = Language.query.get(id)
if db_language is None: if db_language is None:
db.session.add(Language(id=iso["639-1"], name=iso["Sprache"])) db.session.add(Language(id=iso["639-1"], name=iso["Sprache"]))
@ -137,8 +136,7 @@ def seed(dev: bool):
skills=[(3, 3), (1, 5)], skills=[(3, 3), (1, 5)],
searchtopics=[3, 1], searchtopics=[3, 1],
languages=[("de", 5), ("fr", 3)], languages=[("de", 5), ("fr", 3)],
address=("Peter Nichtlustig", "Waldweg", "23i", "Hinterhaus", "13337", "Bielefeld", address=("Peter Nichtlustig", "Waldweg", "23i", "Hinterhaus", "13337", "Bielefeld", "Deutschland"),
"Deutschland"),
contacts=[(4, "@peter:wtf-eg.de"), (1, "peter@wtf-eg.de")]) contacts=[(4, "@peter:wtf-eg.de"), (1, "peter@wtf-eg.de")])
seed_user("dirtydieter", seed_user("dirtydieter",

View File

@ -33,7 +33,7 @@ def update_languages(profile, languages_data):
if "id" not in language_data["language"]: if "id" not in language_data["language"]:
continue continue
language = db.session.get(Language, language_data["language"]["id"]) language = Language.query.get(language_data["language"]["id"])
profile_language = ProfileLanguage.query.filter(ProfileLanguage.profile == profile, profile_language = ProfileLanguage.query.filter(ProfileLanguage.profile == profile,
ProfileLanguage.language == language).first() ProfileLanguage.language == language).first()
@ -110,7 +110,7 @@ def update_contacts(profile, contacts_data):
if "id" in contact_data: if "id" in contact_data:
contact_id = int(contact_data["id"]) contact_id = int(contact_data["id"])
contact_ids_to_be_deleted.remove(contact_id) contact_ids_to_be_deleted.remove(contact_id)
contact = db.session.get(Contact, contact_id) contact = Contact.query.get(contact_id)
else: else:
contact = Contact(profile=profile, contacttype=contacttype) contact = Contact(profile=profile, contacttype=contacttype)
db.session.add(contact) db.session.add(contact)
@ -122,7 +122,7 @@ def update_contacts(profile, contacts_data):
def update_profile(user_id: int): def update_profile(user_id: int):
user = db.session.get(User, user_id) user = User.query.get(user_id)
if user is None: if user is None:
return make_response({}, 404) return make_response({}, 404)

View File

@ -10,14 +10,13 @@ from ki.auth import auth
from ki.handlers import find_profiles as find_profiles_handler from ki.handlers import find_profiles as find_profiles_handler
from ki.handlers import update_profile as update_profile_handler from ki.handlers import update_profile as update_profile_handler
from ki.models import ContactType, Language, Skill, Token, User from ki.models import ContactType, Language, Skill, Token, User
from app import app, db from app import app
content_type_svg = "image/svg+xml" content_type_svg = "image/svg+xml"
content_type_png = "image/png" content_type_png = "image/png"
def token_auth(func): def token_auth(func):
@wraps(func) @wraps(func)
def _token_auth(*args, **kwargs): def _token_auth(*args, **kwargs):
auth_header = request.headers.get("Authorization") auth_header = request.headers.get("Authorization")
@ -66,7 +65,7 @@ def handle_completion_request(model, key):
def handle_icon_request(model, id, path): def handle_icon_request(model, id, path):
object = db.session.get(model, id) object = model.query.get(id)
if object is None: if object is None:
return make_response({}, 404) return make_response({}, 404)

View File

@ -31,7 +31,6 @@ class ApiTest(unittest.TestCase):
self.max_skill_id = max_skill.id self.max_skill_id = max_skill.id
def tearDown(self): def tearDown(self):
with app.app_context():
db.drop_all() db.drop_all()
db.engine.dispose() db.engine.dispose()

View File

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

View File

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

View File

@ -8,7 +8,6 @@ from ki.test.ApiTest import ApiTest
class TestLanguagesEndpoint(ApiTest): class TestLanguagesEndpoint(ApiTest):
def test_skills_options(self): def test_skills_options(self):
response = self.client.options("/languages") response = self.client.options("/languages")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -33,7 +32,6 @@ class TestLanguagesEndpoint(ApiTest):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn("Content-Type", response.headers) self.assertIn("Content-Type", response.headers)
self.assertEqual(response.headers["Content-Type"], "image/svg+xml; charset=utf-8") self.assertEqual(response.headers["Content-Type"], "image/svg+xml; charset=utf-8")
response.close()
if __name__ == "main": if __name__ == "main":

View File

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

View File

@ -20,7 +20,6 @@ class TestProfileEndpoint(ApiTest):
self.assertEqual(login_response.status_code, 200) self.assertEqual(login_response.status_code, 200)
self.assertIn("token", login_response.json) self.assertIn("token", login_response.json)
with app.app_context():
babsi = User.query.filter(User.auth_id == "babsi1").first() babsi = User.query.filter(User.auth_id == "babsi1").first()
response = self.client.post(f"/users/{babsi.id}/profile", response = self.client.post(f"/users/{babsi.id}/profile",
data=json.dumps({}), data=json.dumps({}),
@ -104,7 +103,6 @@ class TestProfileEndpoint(ApiTest):
"level": 2 "level": 2
}] }]
} }
with app.app_context():
peter = User.query.filter(User.auth_id == "peter").first() peter = User.query.filter(User.auth_id == "peter").first()
response = self.client.post(f"/users/{peter.id}/profile", response = self.client.post(f"/users/{peter.id}/profile",
data=json.dumps(data), data=json.dumps(data),
@ -187,7 +185,6 @@ class TestProfileEndpoint(ApiTest):
def test_get_visible_proifle(self): def test_get_visible_proifle(self):
token = self.login("peter", "geheim")["token"] token = self.login("peter", "geheim")["token"]
with app.app_context():
babsi = User.query.filter(User.auth_id == "babsi1").first() 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(f"/users/{babsi.id}/profile", headers={"Authorization": f"Bearer {token}"})
@ -200,7 +197,6 @@ class TestProfileEndpoint(ApiTest):
self.assertEqual(login_response.status_code, 200) self.assertEqual(login_response.status_code, 200)
self.assertIn("token", login_response.json) self.assertIn("token", login_response.json)
with app.app_context():
peter = User.query.filter(User.auth_id == "peter").first() peter = User.query.filter(User.auth_id == "peter").first()
response = self.client.get(f"/users/{peter.id}/profile", response = self.client.get(f"/users/{peter.id}/profile",
headers={"Authorization": "Bearer " + login_response.json["token"]}) headers={"Authorization": "Bearer " + login_response.json["token"]})

View File

@ -8,7 +8,6 @@ from ki.test.ApiTest import ApiTest
class TestSkillsEndpoint(ApiTest): class TestSkillsEndpoint(ApiTest):
def test_skills_options(self): def test_skills_options(self):
response = self.client.options("/skills") response = self.client.options("/skills")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -41,14 +40,12 @@ class TestSkillsEndpoint(ApiTest):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn("Content-Type", response.headers) self.assertIn("Content-Type", response.headers)
self.assertEqual(response.headers["Content-Type"], "image/svg+xml; charset=utf-8") self.assertEqual(response.headers["Content-Type"], "image/svg+xml; charset=utf-8")
response.close()
def test_get_fallback_skill_icon(self): def test_get_fallback_skill_icon(self):
response = self.client.get("/skills/2/icon") response = self.client.get("/skills/2/icon")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn("Content-Type", response.headers) self.assertIn("Content-Type", response.headers)
self.assertEqual(response.headers["Content-Type"], "image/svg+xml; charset=utf-8") self.assertEqual(response.headers["Content-Type"], "image/svg+xml; charset=utf-8")
response.close()
if __name__ == "main": if __name__ == "main":

View File

@ -19,7 +19,7 @@ logger = logging.getLogger('alembic.env')
# target_metadata = mymodel.Base.metadata # target_metadata = mymodel.Base.metadata
config.set_main_option( config.set_main_option(
'sqlalchemy.url', 'sqlalchemy.url',
str(current_app.extensions['migrate'].db.engine.url).replace( str(current_app.extensions['migrate'].db.get_engine().url).replace(
'%', '%%')) '%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata target_metadata = current_app.extensions['migrate'].db.metadata
@ -68,7 +68,7 @@ def run_migrations_online():
directives[:] = [] directives[:] = []
logger.info('No changes in schema detected.') logger.info('No changes in schema detected.')
connectable = current_app.extensions['migrate'].db.engine connectable = current_app.extensions['migrate'].db.get_engine()
with connectable.connect() as connection: with connectable.connect() as connection:
context.configure( context.configure(