Browse Source

switch to using an sqlite db instead of flat file...

it'll make other parts of the system easier to manage having
this infrastructure in place...
main
John-Mark Gurney 4 years ago
parent
commit
c2e59e761f
4 changed files with 118 additions and 51 deletions
  1. +81
    -15
      bitelab/__init__.py
  2. +1
    -36
      bitelab/config.py
  3. +34
    -0
      bitelab/data.py
  4. +2
    -0
      setup.py

+ 81
- 15
bitelab/__init__.py View File

@@ -7,11 +7,13 @@ from httpx import AsyncClient, Auth
from starlette.status import HTTP_200_OK, HTTP_404_NOT_FOUND, HTTP_401_UNAUTHORIZED

from . import config
from . import data

import asyncio
import gc
import socket
import sys
import tempfile
import unittest

# fix up parse_socket_addr for hypercorn
@@ -26,13 +28,26 @@ def new_parse_socket_addr(domain, addr):
tcp_server.parse_socket_addr = new_parse_socket_addr

class BoardManager(object):
board_classes = [ 'cora-z7s' ]
board_class_info = {
'cora-z7s': {
'arch': 'arm64-aarch64',
},
}

# Naming scheme:
# <abbreviated class>-<num>
#
boards = {
'cora-1': {
'class': 'cora-z7s',
}
}

def __init__(self, settings):
self._settings = settings

def classes(self):
return self.board_classes
return self.board_class_info

def unhashable_lru():
def newwrapper(fun):
@@ -72,15 +87,22 @@ class BiteAuth(Auth):
def get_settings():
return config.Settings()

@unhashable_lru()
def get_data(settings: config.Settings = Depends(get_settings)):
print(repr(settings))
database = data.databases.Database('sqlite:///' + settings.db_file)
d = data.make_orm(self.database)
return d

@unhashable_lru()
def get_boardmanager(settings: config.Settings = Depends(get_settings)):
return BoardManager(settings)

oauth2_scheme = OAuth2PasswordBearer(tokenUrl='/nonexistent')

def lookup_user(token: str = Depends(oauth2_scheme), settings: config.Settings = Depends(get_settings)):
async def lookup_user(token: str = Depends(oauth2_scheme), data: data.DataWrapper = Depends(get_data)):
try:
return settings.apikeytouser(token)
return (await data.APIKey.objects.get(key=token)).user
except KeyError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -101,7 +123,7 @@ async def foo(user: str = Depends(lookup_user), brdmgr: BoardManager = Depends(g

@router.get('/board_info')
async def foo(user: str = Depends(lookup_user), brdmgr: BoardManager = Depends(get_boardmanager)):
return brdmgr.classes()
return brdmgr.boards

@router.get('/')
async def foo(board_prio: dict = Depends(board_priority), settings: config.Settings = Depends(get_settings)):
@@ -141,18 +163,37 @@ class TestUnhashLRU(unittest.TestCase):
# does not return the same object as the first cache
self.assertIsNot(cachefun(lsta), cachefun2(lsta))

async def _setup_data(data):
await data.APIKey.objects.create(user='foo', key='thisisanapikey')
await data.APIKey.objects.create(user='bar', key='anotherlongapikey')

# Note: this will not work under python before 3.8 before
# IsolatedAsyncioTestCase was added. The tearDown has to happen
# with the event loop running, otherwise the task and other things
# do not get cleaned up properly.
class TestBiteLab(unittest.IsolatedAsyncioTestCase):
async def get_settings_override(self):
# Note: this gets run on each request.
return config.Settings(apikeyfile="fixtures/api_keys")
def get_settings_override(self):
return self.settings

def setUp(self):
def get_data_override(self):
return self.data

async def asyncSetUp(self):
self.app = getApp()

# setup test database
self.dbtempfile = tempfile.NamedTemporaryFile()
self.database = data.databases.Database('sqlite:///' + self.dbtempfile.name)
self.data = data.make_orm(self.database)

await _setup_data(self.data)

# setup settings
self.settings = config.Settings(db_file=self.dbtempfile.name)

self.app.dependency_overrides[get_settings] = self.get_settings_override
self.app.dependency_overrides[get_data] = self.get_data_override

self.client = AsyncClient(app=self.app, base_url='http://testserver')

def tearDown(self):
@@ -160,11 +201,6 @@ class TestBiteLab(unittest.IsolatedAsyncioTestCase):
asyncio.run(self.client.aclose())
self.client = None

async def test_config(self):
settings = await self.get_settings_override()
self.assertEqual(settings.apikeytouser('thisisanapikey'), 'foo')
self.assertEqual(settings.apikeytouser('anotherlongapikey'), 'bar')

async def test_basic(self):
res = await self.client.get('/')
self.assertNotEqual(res.status_code, HTTP_404_NOT_FOUND)
@@ -180,4 +216,34 @@ class TestBiteLab(unittest.IsolatedAsyncioTestCase):
async def test_classes(self):
res = await self.client.get('/board_classes', auth=BiteAuth('thisisanapikey'))
self.assertEqual(res.status_code, HTTP_200_OK)
self.assertEqual(res.json(), [ 'cora-z7s' ])
self.assertEqual(res.json(), { 'cora-z7s': { 'arch': 'arm64-aarch64', } })

async def test_board_info(self):
res = await self.client.get('/board_info', auth=BiteAuth('thisisanapikey'))
self.assertEqual(res.status_code, HTTP_200_OK)
info = {
'cora-1': {
'class': 'cora-z7s',
},
}
self.assertEqual(res.json(), info)

class TestData(unittest.IsolatedAsyncioTestCase):
def setUp(self):
# setup temporary directory
self.dbtempfile = tempfile.NamedTemporaryFile()

self.database = data.databases.Database('sqlite:///' + self.dbtempfile.name)
self.data = data.make_orm(self.database)

def tearDown(self):
self.data = None
self.database = None
self.dbtempfile = None

async def test_apikey(self):
data = self.data
self.assertEqual(await data.APIKey.objects.all(), [])
await _setup_data(data)
self.assertEqual((await data.APIKey.objects.get(key='thisisanapikey')).user, 'foo')
self.assertEqual((await data.APIKey.objects.get(key='anotherlongapikey')).user, 'bar')

+ 1
- 36
bitelab/config.py View File

@@ -6,42 +6,7 @@ import aiokq
# How to deal w/ private vars:
# https://web.archive.org/web/20201113005838/https://github.com/samuelcolvin/pydantic/issues/655
class Settings(BaseSettings):
__slots__ = ('_apikeydb', '_updatetask', '_fp', )
apikeyfile: str

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

fp = open(self.apikeyfile)
self._sync_updateapikeys(fp)
updtsk = asyncio.create_task(aiokq.run_on_modify(fp,
self._updateapikeys))

object.__setattr__(self, '_fp', fp)
object.__setattr__(self, '_updatetask', updtsk)

# Mark this as covered because coverage says the code isn't run,
# despite the fact that it does get run (and can trigger an
# exception)
def __del__(self): # pragma: no cover
self._updatetask.cancel()
# XXX - looks like task is done before getting here
self._fp.close()

async def _updateapikeys(self, fp):
self._sync_updateapikeys(fp)

def _sync_updateapikeys(self, fp):
fp.seek(0)
keydb = {}
for line in fp.readlines():
user, key = line.split()
keydb[key] = user

object.__setattr__(self, '_apikeydb', keydb)

def apikeytouser(self, key):
return self._apikeydb[key]
db_file: str

class Config:
env_file = ".env"

+ 34
- 0
bitelab/data.py View File

@@ -0,0 +1,34 @@
import databases
import orm
import sqlalchemy

def _issubclass(a, b):
try:
return issubclass(a, b)
except TypeError:
return False

class DataWrapper(object):
pass

def make_orm(database):
metadata = sqlalchemy.MetaData()

class APIKey(orm.Model):
__tablename__ = 'apikeys'
__database__ = database
__metadata__ = metadata

user = orm.Text(index=True)
key = orm.Text(primary_key=True)

engine = sqlalchemy.create_engine(str(database.url))
metadata.create_all(engine)

r = DataWrapper()

lcls = locals()
for i in [ 'engine', 'metadata', ] + [ x for x in lcls if _issubclass(lcls[x], orm.Model) ]:
setattr(r, i, lcls[i])

return r

+ 2
- 0
setup.py View File

@@ -23,6 +23,8 @@ setup(
'hypercorn', # option, for server only?
'pydantic[dotenv]',
'aiokq @ git+https://www.funkthat.com/gitea/jmg/aiokq.git'
'orm',
'databases[sqlite]',
],
extras_require = {
'dev': [ 'coverage' ],


Loading…
Cancel
Save