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
|
# 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 import config
|
||||||
|
from scopes.storage import common
|
||||||
|
|
||||||
|
type Storage = Annotated[common.Storage, Depends(storage)]
|
||||||
|
|
||||||
def storage():
|
def storage():
|
||||||
stf = config.StorageFactory(config.settings)
|
stf = config.StorageFactory(config.settings)
|
||||||
return stf(config.settings.dbschema)
|
return stf(config.settings.dbschema)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from fastapi.responses import HTMLResponse
|
||||||
import transaction
|
import transaction
|
||||||
|
|
||||||
from scopes.api import tracking
|
from scopes.api import tracking
|
||||||
|
from scopes.api.auth import user
|
||||||
|
|
||||||
# app and routes
|
# app and routes
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@ async def handle_transactions(request: Request, call_next):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
app.include_router(tracking.router)
|
app.include_router(tracking.router)
|
||||||
|
app.include_router(user.router)
|
||||||
|
|
||||||
@app.get('/')
|
@app.get('/')
|
||||||
async def read_root():
|
async def read_root():
|
||||||
|
|
|
||||||
|
|
@ -4,35 +4,30 @@
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from fastapi import APIRouter, Body, Depends
|
from fastapi import APIRouter, Body, Depends
|
||||||
from fastapi.responses import HTMLResponse
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
import transaction
|
|
||||||
from typing import Annotated, Any, Self
|
from typing import Annotated, Any, Self
|
||||||
|
|
||||||
from scopes.api import deps
|
from scopes.api import deps
|
||||||
from scopes.storage import tracking
|
from scopes.storage import tracking
|
||||||
from scopes.storage.common import Storage
|
|
||||||
|
|
||||||
class Track(BaseModel):
|
class Track(BaseModel):
|
||||||
uid: str = None
|
uid: str | None = None
|
||||||
head: dict[str, str]
|
head: dict[str, str]
|
||||||
data: dict[str, Any]
|
data: dict[str, Any]
|
||||||
timeStamp: datetime = None
|
timeStamp: datetime | None = None
|
||||||
|
|
||||||
# routes
|
# routes
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
type Storage = Annotated[Storage, Depends(deps.storage)]
|
|
||||||
|
|
||||||
@router.get('/tracks', response_model=list[Track])
|
@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)
|
tracks = storage.create(tracking.Container)
|
||||||
result = [tr.asDict() for tr in tracks.query()]
|
result = [tr.asDict() for tr in tracks.query()]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@router.post('/tracks', response_model=Track)
|
@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)
|
tracks = storage.create(tracking.Container)
|
||||||
hd = track.head
|
hd = track.head
|
||||||
tr = tracking.Track(hd['taskId'], hd['userName'], data=track.data)
|
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 import Integer
|
||||||
from sqlalchemy.dialects.sqlite import JSON
|
from sqlalchemy.dialects.sqlite import JSON
|
||||||
import threading
|
import threading
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class Storage(object):
|
class Storage(object):
|
||||||
|
|
@ -85,7 +86,7 @@ class StorageFactory:
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
IdType = Integer
|
IdType = Integer
|
||||||
JsonType = JSON
|
JsonType: Any = JSON
|
||||||
|
|
||||||
storageClass = Storage
|
storageClass = Storage
|
||||||
|
|
||||||
|
|
@ -101,7 +102,7 @@ class StorageFactory:
|
||||||
|
|
||||||
# store information about container implementations, identified by a uid prefix.
|
# store information about container implementations, identified by a uid prefix.
|
||||||
|
|
||||||
registry = {}
|
registry: dict[str, Any] = {}
|
||||||
|
|
||||||
def registerContainerClass(cls):
|
def registerContainerClass(cls):
|
||||||
prefix = cls.itemFactory.prefix
|
prefix = cls.itemFactory.prefix
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,10 @@ from sqlalchemy.orm import scoped_session, sessionmaker
|
||||||
import transaction
|
import transaction
|
||||||
from zope.sqlalchemy import register, mark_changed
|
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):
|
def sessionFactory(self):
|
||||||
Session = scoped_session(sessionmaker(bind=self.engine, twophase=True))
|
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,
|
return dict(uid=self.uid, head=self.head, data=self.data,
|
||||||
timeStamp=str(self.timeStamp)[:19])
|
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
|
@property
|
||||||
def uid(self):
|
def uid(self):
|
||||||
if self.trackId is None:
|
if self.trackId is None:
|
||||||
|
|
@ -53,7 +60,7 @@ class Record(shape.Record):
|
||||||
|
|
||||||
class Track(Record):
|
class Track(Record):
|
||||||
|
|
||||||
headFields = ['taskId', 'userName']
|
headFields = ('taskId', 'userName')
|
||||||
prefix = 'rec'
|
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
|
# tests/test.env
|
||||||
|
|
||||||
|
LOG_FILE=tests/log/scopes-test.log
|
||||||
|
|
||||||
BASE_URL=http://test
|
BASE_URL=http://test
|
||||||
|
|
||||||
DBNAME=testdb
|
DBNAME=testdb
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,14 @@ from httpx2 import ASGITransport, AsyncClient
|
||||||
|
|
||||||
from scopes.api import main
|
from scopes.api import main
|
||||||
from scopes.api.main import app
|
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')
|
@pytest.fixture(scope='module')
|
||||||
def storage():
|
def storage():
|
||||||
storage = config.StorageFactory(config.settings)(config.settings.dbschema)
|
storage = config.StorageFactory(config.settings)(config.settings.dbschema)
|
||||||
storage.dropTable('tracks')
|
storage.dropTable('tracks')
|
||||||
|
storage.dropTable('users')
|
||||||
storage.commit()
|
storage.commit()
|
||||||
return storage
|
return storage
|
||||||
|
|
||||||
|
|
@ -57,3 +59,12 @@ def test_tracking(storage, caplog):
|
||||||
#print('\n*** Logs: ', caplog.text)
|
#print('\n*** Logs: ', caplog.text)
|
||||||
#print('*** tr02', tr02)
|
#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
|
from datetime import datetime
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from scopes.api import main
|
|
||||||
from scopes.storage import concept, folder, message, topic, tracking
|
from scopes.storage import concept, folder, message, topic, tracking
|
||||||
from scopes.util import keywords as k
|
from scopes.util import keywords as k
|
||||||
|
|
||||||
|
|
@ -89,7 +88,7 @@ def test_type(storage):
|
||||||
concept.setupCoreTypes(storage)
|
concept.setupCoreTypes(storage)
|
||||||
types = storage.getContainer(concept.Type)
|
types = storage.getContainer(concept.Type)
|
||||||
tps = list(types.query())
|
tps = list(types.query())
|
||||||
assert len(tps) == 7
|
assert len(tps) == 8
|
||||||
tfolder = types.queryLast(name='folder')
|
tfolder = types.queryLast(name='folder')
|
||||||
fldrs = list(tfolder.values())
|
fldrs = list(tfolder.values())
|
||||||
assert len(fldrs) == 2
|
assert len(fldrs) == 2
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue