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
"""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.storage import common
type Storage = Annotated[common.Storage, Depends(storage)]
def storage():
stf = config.StorageFactory(config.settings)
return stf(config.settings.dbschema)

View file

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

View file

@ -4,35 +4,30 @@
from datetime import datetime
from fastapi import APIRouter, Body, Depends
from fastapi.responses import HTMLResponse
from pydantic import BaseModel, Field
import transaction
from typing import Annotated, Any, Self
from scopes.api import deps
from scopes.storage import tracking
from scopes.storage.common import Storage
class Track(BaseModel):
uid: str = None
uid: str | None = None
head: dict[str, str]
data: dict[str, Any]
timeStamp: datetime = None
timeStamp: datetime | None = None
# routes
router = APIRouter()
type Storage = Annotated[Storage, Depends(deps.storage)]
@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)
result = [tr.asDict() for tr in tracks.query()]
return result
@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)
hd = track.head
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.dialects.sqlite import JSON
import threading
from typing import Any
class Storage(object):
@ -85,7 +86,7 @@ class StorageFactory:
conn.commit()
IdType = Integer
JsonType = JSON
JsonType: Any = JSON
storageClass = Storage
@ -101,7 +102,7 @@ class StorageFactory:
# store information about container implementations, identified by a uid prefix.
registry = {}
registry: dict[str, Any] = {}
def registerContainerClass(cls):
prefix = cls.itemFactory.prefix

View file

@ -9,10 +9,10 @@ from sqlalchemy.orm import scoped_session, sessionmaker
import transaction
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):
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,
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
def uid(self):
if self.trackId is None:
@ -53,7 +60,7 @@ class Record(shape.Record):
class Track(Record):
headFields = ['taskId', 'userName']
headFields = ('taskId', 'userName')
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
LOG_FILE=tests/log/scopes-test.log
BASE_URL=http://test
DBNAME=testdb

View file

@ -11,12 +11,14 @@ from httpx2 import ASGITransport, AsyncClient
from scopes.api import main
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')
def storage():
storage = config.StorageFactory(config.settings)(config.settings.dbschema)
storage.dropTable('tracks')
storage.dropTable('users')
storage.commit()
return storage
@ -57,3 +59,12 @@ def test_tracking(storage, caplog):
#print('\n*** Logs: ', caplog.text)
#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
import pytest
from scopes.api import main
from scopes.storage import concept, folder, message, topic, tracking
from scopes.util import keywords as k
@ -89,7 +88,7 @@ def test_type(storage):
concept.setupCoreTypes(storage)
types = storage.getContainer(concept.Type)
tps = list(types.query())
assert len(tps) == 7
assert len(tps) == 8
tfolder = types.queryLast(name='folder')
fldrs = list(tfolder.values())
assert len(fldrs) == 2