From 7d309491fc545dae4f6c46e8b85bfeed7a754cb4 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Fri, 6 Nov 2026 09:32:01 +0100 Subject: [PATCH] fixes and improvements, make mypy happy, prepayre auth stuff --- pyproject.toml | 3 +++ scopes/api/auth/user.py | 9 ++++++++- scopes/api/deps.py | 4 ++++ scopes/api/main.py | 2 +- scopes/config.py | 2 +- scopes/shape.py | 6 ++++++ scopes/storage/concept.py | 22 +++++++++++----------- scopes/storage/db/postgres.py | 4 ++-- scopes/storage/folder.py | 4 ++-- scopes/storage/proxy.py | 2 +- scopes/storage/tracking.py | 7 ++++--- scopes/storage/user.py | 1 - tests/test.env | 8 +++++++- 13 files changed, 50 insertions(+), 24 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 691ef76..4a2b7ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,3 +42,6 @@ testpaths = ["tests"] #python_files = ["test_api.py"] # use .pytest.ini file for local test config +[tool.mypy] +follow_imports = 'skip' + diff --git a/scopes/api/auth/user.py b/scopes/api/auth/user.py index 2af8b84..c71e8ef 100644 --- a/scopes/api/auth/user.py +++ b/scopes/api/auth/user.py @@ -1,6 +1,7 @@ # scopes.api.auth user from fastapi import APIRouter +import jwt from pwdlib import PasswordHash from pydantic import BaseModel, Field from typing import List @@ -17,7 +18,7 @@ class User(BaseModel): pw_hash: str | None = Field(exclude=True, default=None) groups: List[str] = Field(default_factory=list) -# password handling +# password handling, authentication pw_hash = PasswordHash.recommended() @@ -27,6 +28,12 @@ def hashPassword(pw): def verifyPassword(plain, hashed): return pw_hash.verify(plain, hashed) +async def authenticate(storage: deps.Storage, token: deps.Auth): + payload = jwt.decode(token, settings.jwt_key, algorithms=[settings.jwt_algorithm]) + userName = payload.get('sub') + user = getUser(storage, userName) + return user + # routes router = APIRouter() diff --git a/scopes/api/deps.py b/scopes/api/deps.py index 77d8f83..01349e5 100644 --- a/scopes/api/deps.py +++ b/scopes/api/deps.py @@ -9,8 +9,12 @@ from scopes import config from scopes.storage import common type Storage = Annotated[common.Storage, Depends(storage)] +type Auth = Annotated[str, Depends(authenticator)] def storage(): stf = config.StorageFactory(config.settings) return stf(config.settings.dbschema) +def authenticator(): + return config.Authenticator() + diff --git a/scopes/api/main.py b/scopes/api/main.py index 664252e..4849b6d 100644 --- a/scopes/api/main.py +++ b/scopes/api/main.py @@ -6,7 +6,7 @@ if '' not in sys.path: from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse -import transaction +import transaction # type: ignore from scopes.api import tracking from scopes.api.auth import user diff --git a/scopes/config.py b/scopes/config.py index 80ab52c..077f008 100644 --- a/scopes/config.py +++ b/scopes/config.py @@ -1,4 +1,4 @@ -# py-scopes/apidemo/config.py +# scopes.config.py from pydantic_settings import BaseSettings, SettingsConfigDict from scopes.storage.db.postgres import StorageFactory diff --git a/scopes/shape.py b/scopes/shape.py index 0c66295..7b393f9 100644 --- a/scopes/shape.py +++ b/scopes/shape.py @@ -55,3 +55,9 @@ class Record: def toObject(self, cls): return cls(**self.asDict(flatten=True)) + @classmethod + def fromObject(cls, obj): + inp = obj.model_dump() + headValues = [inp.pop(hf) for hf in cls.headFields] + return cls(*headValues, data=inp) + diff --git a/scopes/storage/concept.py b/scopes/storage/concept.py index 33a8e2f..be8c2aa 100644 --- a/scopes/storage/concept.py +++ b/scopes/storage/concept.py @@ -2,17 +2,17 @@ """Core classes for concept map structure.""" -from zope.interface import implementer +from zope.interface import implementer # type: ignore from scopes.interfaces import IContainer from scopes.storage.common import registerContainerClass, registry -from scopes.storage.tracking import Container, Track +from scopes.storage import tracking defaultPredicate = 'standard' -class Concept(Track): +class Concept(tracking.Track): - headFields = ['name'] + headFields: tuple[str, ...] = ('name',) def parents(self, predicate=None): return (r.getFirst() for r in self.parentRels(predicate)) @@ -34,17 +34,17 @@ class Concept(Track): rels.save(Triple(self.uid, child.uid, predicate)) -class Concepts(Container): +class Concepts(tracking.Container): insertOnChange = False - indexes = None + indexes: tracking.Indexes = None def queryRels(self, **crit): #pred = crit.get(predicate) #if pred is not None and isinstance(pred, ('string', 'bytes')): # crit['predicate'] = self.storage.getContainer(Predicate).queryLast(name=pred) for k, v in crit.items(): - if isinstance(v, Track): + if isinstance(v, tracking.Track): crit[k] = v.uid rels = self.storage.getContainer(Triple) return rels.query(**crit) @@ -69,9 +69,9 @@ def storePredicate(storage, name): preds.save(Predicate(name)) -class Triple(Track): +class Triple(tracking.Track): - headFields = ['first', 'second', 'predicate'] + headFields = ('first', 'second', 'predicate') prefix = 'rel' def getFirst(self): @@ -85,7 +85,7 @@ class Triple(Track): @registerContainerClass -class Rels(Container): +class Rels(tracking.Container): itemFactory = Triple indexes = [('first', 'predicate', 'second'), @@ -99,7 +99,7 @@ class Rels(Container): @implementer(IContainer) class Type(Concept): - headFields = ['name', 'tprefix'] + headFields = ('name', 'tprefix') prefix = 'type' @property diff --git a/scopes/storage/db/postgres.py b/scopes/storage/db/postgres.py index 578b04a..e7616ce 100644 --- a/scopes/storage/db/postgres.py +++ b/scopes/storage/db/postgres.py @@ -6,8 +6,8 @@ from sqlalchemy import create_engine from sqlalchemy import BigInteger from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import scoped_session, sessionmaker -import transaction -from zope.sqlalchemy import register, mark_changed +import transaction # type: ignore +from zope.sqlalchemy import register, mark_changed # type: ignore from scopes.storage.common import StorageFactory as BaseStorageFactory diff --git a/scopes/storage/folder.py b/scopes/storage/folder.py index e8dd61a..98dbca1 100644 --- a/scopes/storage/folder.py +++ b/scopes/storage/folder.py @@ -1,6 +1,6 @@ # scopes.storage.folder -from zope.interface import implementer +from zope.interface import implementer # type: ignore from scopes.interfaces import IContainer, IReference from scopes.storage.common import registerContainerClass @@ -22,7 +22,7 @@ class DummyFolder(dict): @implementer(IContainer, IReference) class Folder(Track): - headFields = ['parent', 'name', 'ref'] + headFields = ('parent', 'name', 'ref') prefix = 'fldr' def values(self): diff --git a/scopes/storage/proxy.py b/scopes/storage/proxy.py index 7281294..7f73437 100644 --- a/scopes/storage/proxy.py +++ b/scopes/storage/proxy.py @@ -6,7 +6,7 @@ in order to store attribute values in a SQL database. This is currently in concept and exploration state. """ -import transaction +import transaction # type: ignore _not_found = object() diff --git a/scopes/storage/tracking.py b/scopes/storage/tracking.py index 544df9c..699f6dd 100644 --- a/scopes/storage/tracking.py +++ b/scopes/storage/tracking.py @@ -15,6 +15,7 @@ from sqlalchemy import and_ from scopes import shape from scopes.storage.common import registerContainerClass +type Indexes = list[tuple[str, ...]] | None class Record(shape.Record): @@ -56,7 +57,7 @@ class Record(shape.Record): class Track(Record): - headFields = ('taskId', 'userName') + headFields: tuple[str, ...] = ('taskId', 'userName') prefix = 'rec' @@ -66,8 +67,8 @@ class Container: itemFactory = Track tableName = 'tracks' insertOnChange = True # always insert new track when data are changed - indexes = None # default, will be overwritten by registerContainerClass() - #indexes = [('username',), ('taskid', 'username')] # or put explicitly in class + indexes: Indexes = None # is set in registerContainerClass() + #indexes = [('username',), ('taskid', 'username')] # explicit setting in subclass table = None diff --git a/scopes/storage/user.py b/scopes/storage/user.py index 8a0f460..6191cbe 100644 --- a/scopes/storage/user.py +++ b/scopes/storage/user.py @@ -9,7 +9,6 @@ class User(concept.Concept): prefix = 'user' - @registerContainerClass class Users(concept.Concepts): diff --git a/tests/test.env b/tests/test.env index 4dad37c..0433048 100644 --- a/tests/test.env +++ b/tests/test.env @@ -9,7 +9,13 @@ DBUSER=testuser DBPASSWORD=secret DBSCHEMA=testing -OIDC_PROVIDER=https://oidc.example.com +AUTHENTICATOR=testing + +# openssl rand -hex 32: +JWT_KEY=391a521ee5ba2b6ce67b8fdbb9f0ca88f40a87d3bc1e82974381b647bb0fc776 +JWT_ALGORITHM=HS256 + +OIDC_PROVIDER=http://oidc.test OIDC_CLIENT_ID=12345 OIDC_COOKIE_CRYPT=secret_crypt OIDC_ORGANIZATION_ID=12346