api, storage: POST user; + more fixes and improvements
This commit is contained in:
parent
7d309491fc
commit
8494f3737d
8 changed files with 39 additions and 12 deletions
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 = ''
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue