api, storage: POST user; + more fixes and improvements

This commit is contained in:
Helmut Merz 2026-11-06 14:29:26 +01:00
parent 7d309491fc
commit 8494f3737d
8 changed files with 39 additions and 12 deletions

View file

@ -29,7 +29,8 @@ zope = [
"zope.traversing", "zope.traversing",
] ]
api = ["fastapi[standard]"] api = ["fastapi[standard]"]
auth = ["authlib", "pyjwt[crypto]", "cryptography", "pwdlib[argon2]", "requests"] auth = ["authlib", "pyjwt[crypto]", "cryptography", "httpx2",
"pwdlib[argon2]", "requests"]
zopetest = ["zope.testrunner"] zopetest = ["zope.testrunner"]
apitest = ["pytest", "httpx2"] apitest = ["pytest", "httpx2"]
@ -43,5 +44,7 @@ testpaths = ["tests"]
# use .pytest.ini file for local test config # use .pytest.ini file for local test config
[tool.mypy] [tool.mypy]
packages = ['scopes.api', 'scopes.core', 'scopes.shape',
'scopes.storage', 'scopes.util']
follow_imports = 'skip' follow_imports = 'skip'

View file

@ -1,13 +1,15 @@
# scopes.api.auth user # scopes.api.auth user
from fastapi import APIRouter from fastapi import APIRouter, Body
import jwt import jwt
from pwdlib import PasswordHash from pwdlib import PasswordHash
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing import List from typing import Annotated, List
from scopes.api import deps from scopes.api import deps
from scopes.config import settings
from scopes.storage import user as st_user from scopes.storage import user as st_user
from scopes.storage.common import Storage
class User(BaseModel): class User(BaseModel):
uid: str | None = None uid: str | None = None
@ -28,6 +30,11 @@ def hashPassword(pw):
def verifyPassword(plain, hashed): def verifyPassword(plain, hashed):
return pw_hash.verify(plain, hashed) return pw_hash.verify(plain, hashed)
def getUser(storage: Storage, userName: str) -> User:
users = storage.create(st_user.Users)
user = users.queryLast(name=userName)
return user.toObject(User)
async def authenticate(storage: deps.Storage, token: deps.Auth): async def authenticate(storage: deps.Storage, token: deps.Auth):
payload = jwt.decode(token, settings.jwt_key, algorithms=[settings.jwt_algorithm]) payload = jwt.decode(token, settings.jwt_key, algorithms=[settings.jwt_algorithm])
userName = payload.get('sub') userName = payload.get('sub')
@ -44,3 +51,10 @@ async def get_users(storage: deps.Storage):
result = [tr.toObject(User) for tr in users.query()] result = [tr.toObject(User) for tr in users.query()]
return result return result
@router.post('/users', response_model=User)
async def post_user(user: Annotated[User, Body()], storage: deps.Storage):
users = storage.create(st_user.Users)
tr = st_user.User.fromObject(user)
users.save(tr)
return tr.toObject(User)

View file

@ -3,7 +3,7 @@
"""basic (generic) routes and views for tracks (records)""" """basic (generic) routes and views for tracks (records)"""
from datetime import datetime from datetime import datetime
from fastapi import APIRouter, Body, Depends from fastapi import APIRouter, Body
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing import Annotated, Any, Self from typing import Annotated, Any, Self

View file

@ -6,7 +6,7 @@ from scopes import shape
class Message(shape.Record): class Message(shape.Record):
headFields = ('domain', 'action', 'class', 'item') headFields: tuple[str, ...] = ('domain', 'action', 'class', 'item')
headFieldsAsKeywords = {'domain', 'action', 'class'} headFieldsAsKeywords = {'domain', 'action', 'class'}
customer = None customer = None

View file

@ -6,7 +6,7 @@ from scopes.util import keywords as k
class Record: class Record:
headFields: tuple = () # to be defined by subclass headFields: tuple[str, ...] = () # to be defined by subclass
headFieldsAsKeywords: set[str] = set() headFieldsAsKeywords: set[str] = set()
defaultHeadValue = '' defaultHeadValue = ''

View file

@ -4,19 +4,18 @@
from scopes.core.message import Message as BaseMessage from scopes.core.message import Message as BaseMessage
from scopes.storage.common import registerContainerClass from scopes.storage.common import registerContainerClass
#from scopes.storage.tracking import Container, Track from scopes.storage import tracking
from scopes.storage.tracking import Container, Record
class Message(Record, BaseMessage): class Message(BaseMessage, tracking.Track):
prefix = 'msg' prefix = 'msg'
@registerContainerClass @registerContainerClass
class Messages(Container): class Messages(tracking.Container):
itemFactory = Message itemFactory: type[Message] = Message
indexes = [('domain', 'action', 'class', 'item'), ('domain', 'class', 'item')] indexes = [('domain', 'action', 'class', 'item'), ('domain', 'class', 'item')]
tableName = 'messages' tableName = 'messages'
insertOnChange = True insertOnChange = True

View file

@ -33,6 +33,12 @@ class Record(shape.Record):
return base | self.head | self.data return base | self.head | self.data
return base | super().asDict() return base | super().asDict()
@classmethod
def fromObject(cls, obj):
tr = super().fromObject(obj)
tr.data.pop('uid', None)
return tr
@property @property
def uid(self): def uid(self):
if self.trackId is None: if self.trackId is None:
@ -64,7 +70,7 @@ class Track(Record):
@registerContainerClass @registerContainerClass
class Container: class Container:
itemFactory = Track itemFactory: type[Track] = Track
tableName = 'tracks' tableName = 'tracks'
insertOnChange = True # always insert new track when data are changed insertOnChange = True # always insert new track when data are changed
indexes: Indexes = None # is set in registerContainerClass() indexes: Indexes = None # is set in registerContainerClass()

View file

@ -72,4 +72,9 @@ def test_user(storage, caplog):
assert len(users) == 1 assert len(users) == 1
u01i = users[0] u01i = users[0]
assert u01i.get(pw_hash) is None assert u01i.get(pw_hash) is None
resp = clt.post('/users', json={'name': 'jane', 'login': 'jane',
'display_name': 'Jane', 'email': 'jane@xmpl.org'})
assert resp.status_code == 200
u02 = resp.json()
assert u02['uid'] == 'user-2'