From 8494f3737d9872ef8f2a41d5e7b06b3001d92432 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Fri, 6 Nov 2026 14:29:26 +0100 Subject: [PATCH] api, storage: POST user; + more fixes and improvements --- pyproject.toml | 5 ++++- scopes/api/auth/user.py | 18 ++++++++++++++++-- scopes/api/tracking.py | 2 +- scopes/core/message.py | 2 +- scopes/shape.py | 2 +- scopes/storage/message.py | 9 ++++----- scopes/storage/tracking.py | 8 +++++++- tests/test_api.py | 5 +++++ 8 files changed, 39 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4a2b7ad..aeb202f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,8 @@ zope = [ "zope.traversing", ] api = ["fastapi[standard]"] -auth = ["authlib", "pyjwt[crypto]", "cryptography", "pwdlib[argon2]", "requests"] +auth = ["authlib", "pyjwt[crypto]", "cryptography", "httpx2", + "pwdlib[argon2]", "requests"] zopetest = ["zope.testrunner"] apitest = ["pytest", "httpx2"] @@ -43,5 +44,7 @@ testpaths = ["tests"] # use .pytest.ini file for local test config [tool.mypy] +packages = ['scopes.api', 'scopes.core', 'scopes.shape', + 'scopes.storage', 'scopes.util'] follow_imports = 'skip' diff --git a/scopes/api/auth/user.py b/scopes/api/auth/user.py index c71e8ef..9122f3e 100644 --- a/scopes/api/auth/user.py +++ b/scopes/api/auth/user.py @@ -1,13 +1,15 @@ # scopes.api.auth user -from fastapi import APIRouter +from fastapi import APIRouter, Body import jwt from pwdlib import PasswordHash from pydantic import BaseModel, Field -from typing import List +from typing import Annotated, List from scopes.api import deps +from scopes.config import settings from scopes.storage import user as st_user +from scopes.storage.common import Storage class User(BaseModel): uid: str | None = None @@ -28,6 +30,11 @@ def hashPassword(pw): def verifyPassword(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): payload = jwt.decode(token, settings.jwt_key, algorithms=[settings.jwt_algorithm]) userName = payload.get('sub') @@ -44,3 +51,10 @@ async def get_users(storage: deps.Storage): result = [tr.toObject(User) for tr in users.query()] 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) + diff --git a/scopes/api/tracking.py b/scopes/api/tracking.py index d8bfc9d..be5f930 100644 --- a/scopes/api/tracking.py +++ b/scopes/api/tracking.py @@ -3,7 +3,7 @@ """basic (generic) routes and views for tracks (records)""" from datetime import datetime -from fastapi import APIRouter, Body, Depends +from fastapi import APIRouter, Body from pydantic import BaseModel, Field from typing import Annotated, Any, Self diff --git a/scopes/core/message.py b/scopes/core/message.py index e04a808..3ae1977 100644 --- a/scopes/core/message.py +++ b/scopes/core/message.py @@ -6,7 +6,7 @@ from scopes import shape class Message(shape.Record): - headFields = ('domain', 'action', 'class', 'item') + headFields: tuple[str, ...] = ('domain', 'action', 'class', 'item') headFieldsAsKeywords = {'domain', 'action', 'class'} customer = None diff --git a/scopes/shape.py b/scopes/shape.py index 7b393f9..6edc0e8 100644 --- a/scopes/shape.py +++ b/scopes/shape.py @@ -6,7 +6,7 @@ from scopes.util import keywords as k class Record: - headFields: tuple = () # to be defined by subclass + headFields: tuple[str, ...] = () # to be defined by subclass headFieldsAsKeywords: set[str] = set() defaultHeadValue = '' diff --git a/scopes/storage/message.py b/scopes/storage/message.py index 7182e65..a1735bf 100644 --- a/scopes/storage/message.py +++ b/scopes/storage/message.py @@ -4,19 +4,18 @@ from scopes.core.message import Message as BaseMessage from scopes.storage.common import registerContainerClass -#from scopes.storage.tracking import Container, Track -from scopes.storage.tracking import Container, Record +from scopes.storage import tracking -class Message(Record, BaseMessage): +class Message(BaseMessage, tracking.Track): prefix = 'msg' @registerContainerClass -class Messages(Container): +class Messages(tracking.Container): - itemFactory = Message + itemFactory: type[Message] = Message indexes = [('domain', 'action', 'class', 'item'), ('domain', 'class', 'item')] tableName = 'messages' insertOnChange = True diff --git a/scopes/storage/tracking.py b/scopes/storage/tracking.py index 699f6dd..981cf20 100644 --- a/scopes/storage/tracking.py +++ b/scopes/storage/tracking.py @@ -33,6 +33,12 @@ class Record(shape.Record): return base | self.head | self.data return base | super().asDict() + @classmethod + def fromObject(cls, obj): + tr = super().fromObject(obj) + tr.data.pop('uid', None) + return tr + @property def uid(self): if self.trackId is None: @@ -64,7 +70,7 @@ class Track(Record): @registerContainerClass class Container: - itemFactory = Track + itemFactory: type[Track] = Track tableName = 'tracks' insertOnChange = True # always insert new track when data are changed indexes: Indexes = None # is set in registerContainerClass() diff --git a/tests/test_api.py b/tests/test_api.py index 9e95fc3..69eefcf 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -72,4 +72,9 @@ def test_user(storage, caplog): assert len(users) == 1 u01i = users[0] 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'