api, work in progress: authn/authz, starting with user info
This commit is contained in:
parent
dc3acc5da5
commit
f7ba9c3881
12 changed files with 89 additions and 19 deletions
1
scopes/api/auth/__init__.py
Normal file
1
scopes/api/auth/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""package scopes.api.auth"""
|
||||
28
scopes/api/auth/user.py
Normal file
28
scopes/api/auth/user.py
Normal 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
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
19
scopes/storage/user.py
Normal 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'
|
||||
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
# tests/test.env
|
||||
|
||||
LOG_FILE=tests/log/scopes-test.log
|
||||
|
||||
BASE_URL=http://test
|
||||
|
||||
DBNAME=testdb
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue