api, work in progress: authn/authz, starting with user info

This commit is contained in:
Helmut Merz 2026-11-04 11:54:24 +01:00
parent dc3acc5da5
commit f7ba9c3881
12 changed files with 89 additions and 19 deletions

View file

@ -0,0 +1 @@
"""package scopes.api.auth"""

28
scopes/api/auth/user.py Normal file
View file

@ -0,0 +1,28 @@
# scopes.api.auth user
from fastapi import APIRouter
from pydantic import BaseModel, Field
from typing import List
from scopes.api import deps
from scopes.storage import user as st_user
class User(BaseModel):
uid: str | None = None
name: str
login: str
display_name: str
email: str | None = None
pw_hash: str | None = None
groups: List[str] = Field(default_factory=list)
# routes
router = APIRouter()
@router.get('/users', response_model=list[User])
async def get_users(storage: deps.Storage):
users = storage.create(st_user.Users)
result = [tr.toModel(User) for tr in users.query()]
return result

View file

@ -1,11 +1,16 @@
# scopes.api.deps # scopes.api.deps
"""Common API dependencies""" """Common dependencies and other definitions for Web API"""
from fastapi import Depends
from typing import Annotated
from scopes import config from scopes import config
from scopes.storage import common
type Storage = Annotated[common.Storage, Depends(storage)]
def storage(): def storage():
stf = config.StorageFactory(config.settings) stf = config.StorageFactory(config.settings)
return stf(config.settings.dbschema) return stf(config.settings.dbschema)

View file

@ -9,6 +9,7 @@ from fastapi.responses import HTMLResponse
import transaction import transaction
from scopes.api import tracking from scopes.api import tracking
from scopes.api.auth import user
# app and routes # app and routes
@ -24,6 +25,7 @@ async def handle_transactions(request: Request, call_next):
return response return response
app.include_router(tracking.router) app.include_router(tracking.router)
app.include_router(user.router)
@app.get('/') @app.get('/')
async def read_root(): async def read_root():

View file

@ -4,35 +4,30 @@
from datetime import datetime from datetime import datetime
from fastapi import APIRouter, Body, Depends from fastapi import APIRouter, Body, Depends
from fastapi.responses import HTMLResponse
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
import transaction
from typing import Annotated, Any, Self from typing import Annotated, Any, Self
from scopes.api import deps from scopes.api import deps
from scopes.storage import tracking from scopes.storage import tracking
from scopes.storage.common import Storage
class Track(BaseModel): class Track(BaseModel):
uid: str = None uid: str | None = None
head: dict[str, str] head: dict[str, str]
data: dict[str, Any] data: dict[str, Any]
timeStamp: datetime = None timeStamp: datetime | None = None
# routes # routes
router = APIRouter() router = APIRouter()
type Storage = Annotated[Storage, Depends(deps.storage)]
@router.get('/tracks', response_model=list[Track]) @router.get('/tracks', response_model=list[Track])
async def get_tracks(storage: Storage): async def get_tracks(storage: deps.Storage):
tracks = storage.create(tracking.Container) tracks = storage.create(tracking.Container)
result = [tr.asDict() for tr in tracks.query()] result = [tr.asDict() for tr in tracks.query()]
return result return result
@router.post('/tracks', response_model=Track) @router.post('/tracks', response_model=Track)
async def post_track(track: Annotated[Track, Body()], storage: Storage): async def post_track(track: Annotated[Track, Body()], storage: deps.Storage):
tracks = storage.create(tracking.Container) tracks = storage.create(tracking.Container)
hd = track.head hd = track.head
tr = tracking.Track(hd['taskId'], hd['userName'], data=track.data) tr = tracking.Track(hd['taskId'], hd['userName'], data=track.data)

View file

@ -7,6 +7,7 @@ from sqlalchemy import create_engine, MetaData, text
from sqlalchemy import Integer from sqlalchemy import Integer
from sqlalchemy.dialects.sqlite import JSON from sqlalchemy.dialects.sqlite import JSON
import threading import threading
from typing import Any
class Storage(object): class Storage(object):
@ -85,7 +86,7 @@ class StorageFactory:
conn.commit() conn.commit()
IdType = Integer IdType = Integer
JsonType = JSON JsonType: Any = JSON
storageClass = Storage storageClass = Storage
@ -101,7 +102,7 @@ class StorageFactory:
# store information about container implementations, identified by a uid prefix. # store information about container implementations, identified by a uid prefix.
registry = {} registry: dict[str, Any] = {}
def registerContainerClass(cls): def registerContainerClass(cls):
prefix = cls.itemFactory.prefix prefix = cls.itemFactory.prefix

View file

@ -9,10 +9,10 @@ from sqlalchemy.orm import scoped_session, sessionmaker
import transaction import transaction
from zope.sqlalchemy import register, mark_changed from zope.sqlalchemy import register, mark_changed
from scopes.storage.common import StorageFactory from scopes.storage.common import StorageFactory as BaseStorageFactory
class StorageFactory(StorageFactory): class StorageFactory(BaseStorageFactory):
def sessionFactory(self): def sessionFactory(self):
Session = scoped_session(sessionmaker(bind=self.engine, twophase=True)) Session = scoped_session(sessionmaker(bind=self.engine, twophase=True))

View file

@ -29,6 +29,13 @@ class Record(shape.Record):
return dict(uid=self.uid, head=self.head, data=self.data, return dict(uid=self.uid, head=self.head, data=self.data,
timeStamp=str(self.timeStamp)[:19]) timeStamp=str(self.timeStamp)[:19])
def toModel(self, cls):
data = dict(uid=self.uid)
data.update(self.head)
for k, v in self.data.items():
data[k] = v
return cls(**data)
@property @property
def uid(self): def uid(self):
if self.trackId is None: if self.trackId is None:
@ -53,7 +60,7 @@ class Record(shape.Record):
class Track(Record): class Track(Record):
headFields = ['taskId', 'userName'] headFields = ('taskId', 'userName')
prefix = 'rec' prefix = 'rec'

19
scopes/storage/user.py Normal file
View file

@ -0,0 +1,19 @@
# scopes.storage.user
from scopes.storage.common import registerContainerClass
from scopes.storage import concept
class User(concept.Concept):
headFields = ('name', 'login')
prefix = 'user'
@registerContainerClass
class Users(concept.Concepts):
itemFactory = User
indexes = [('name',), ('login',)]
tableName = 'users'

View file

@ -1,5 +1,7 @@
# tests/test.env # tests/test.env
LOG_FILE=tests/log/scopes-test.log
BASE_URL=http://test BASE_URL=http://test
DBNAME=testdb DBNAME=testdb

View file

@ -11,12 +11,14 @@ from httpx2 import ASGITransport, AsyncClient
from scopes.api import main from scopes.api import main
from scopes.api.main import app from scopes.api.main import app
from scopes.storage import tracking from scopes.storage import tracking, user as st_user
from scopes.api.auth import user as api_user
@pytest.fixture(scope='module') @pytest.fixture(scope='module')
def storage(): def storage():
storage = config.StorageFactory(config.settings)(config.settings.dbschema) storage = config.StorageFactory(config.settings)(config.settings.dbschema)
storage.dropTable('tracks') storage.dropTable('tracks')
storage.dropTable('users')
storage.commit() storage.commit()
return storage return storage
@ -57,3 +59,12 @@ def test_tracking(storage, caplog):
#print('\n*** Logs: ', caplog.text) #print('\n*** Logs: ', caplog.text)
#print('*** tr02', tr02) #print('*** tr02', tr02)
def test_user(storage, caplog):
users = storage.create(st_user.Users)
u01 = st_user.User('jim', 'jim', data=dict(display_name='Jim'))
u01id = users.save(u01)
storage.commit()
with TestClient(app) as clt:
caplog.set_level(config.settings.log_level)
users = clt.get('/users').json()
assert len(users) == 1

View file

@ -8,7 +8,6 @@ config.settings = config.Settings(_env_file='tests/test.env')
from datetime import datetime from datetime import datetime
import pytest import pytest
from scopes.api import main
from scopes.storage import concept, folder, message, topic, tracking from scopes.storage import concept, folder, message, topic, tracking
from scopes.util import keywords as k from scopes.util import keywords as k
@ -89,7 +88,7 @@ def test_type(storage):
concept.setupCoreTypes(storage) concept.setupCoreTypes(storage)
types = storage.getContainer(concept.Type) types = storage.getContainer(concept.Type)
tps = list(types.query()) tps = list(types.query())
assert len(tps) == 7 assert len(tps) == 8
tfolder = types.queryLast(name='folder') tfolder = types.queryLast(name='folder')
fldrs = list(tfolder.values()) fldrs = list(tfolder.values())
assert len(fldrs) == 2 assert len(fldrs) == 2