diff --git a/ui/Makefile b/ui/Makefile index 7127d56..bb08dd5 100644 --- a/ui/Makefile +++ b/ui/Makefile @@ -1,9 +1,9 @@ -VIRTUALENV?=virtualenv-3.7 -MODULES=cli.py kleintest.py mtree.py server.py +VIRTUALENV?=python3.8 -m venv +MODULES=medashare test: fixtures/sample.data.pasn1 fixtures/sample.persona.pasn1 fixtures/sample.mtree (. ./p/bin/activate && \ - (ls $(MODULES) | entr sh -c 'python3 -m coverage run -m unittest -f $(basename $(MODULES)) && coverage report -m --omit=p/\*')) + (find $(MODULES) -type f | entr sh -c 'python3 -m coverage run -m unittest $(MODULES).tests && coverage report -m --omit=p/\*')) env: $(VIRTUALENV) p diff --git a/ui/kleintest.py b/ui/kleintest.py deleted file mode 100644 index f954262..0000000 --- a/ui/kleintest.py +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env python - -from klein import Klein -from klein.interfaces import IKleinRequest -from twisted.internet.defer import Deferred -from twisted.trial import unittest -from twisted.web.http_headers import Headers -from zope.interface import implementer -from io import StringIO -from requests.structures import CaseInsensitiveDict - -__all__ = [ 'FakeRequests', ] - -# https://github.com/twisted/twisted/blob/twisted-19.7.0/src/twisted/web/http.py#L664 -@implementer(IKleinRequest) -class FakeHTTPRequest(object): - - code = 200 - - def __init__(self, meth, uri, data): - - #self.requestHeaders = Headers() - self.responseHeaders = Headers() - - self.content = StringIO(data) - - self.path = uri - self.prepath = [] - self.postpath = uri.split(b'/') - self.method = meth - self.notifications = [] - self.finished = False - - def setHeader(self, name, value): - self.responseHeaders.setRawHeaders(name, [value]) - - def setResponseCode(self, code, message=None): - self.code = code - - def getRequestHostname(self): - return b'' - - def getHost(self): - return b'' - - def isSecure(self): - return False - - def processingFailed(self, failure): - self.setResponseCode(500, 'Internal Server Error') - - #print 'f:', `failure` - #print 'b:', failure.getTraceback() - - def _cleanup(self): - for d in self.notifications: - d.callback(None) - self.notifications = [] - - def finish(self): - if self.finished: # pragma: no cover - warnings.warn('Warning! request.finish called twice.', stacklevel=2) - - self.finished = True - - self._cleanup() - - def notifyFinish(self): - self.notifications.append(Deferred()) - return self.notifications[-1] - -class FakeRequestsResponse(object): - def __init__(self, req): - self._req = req - self._io = StringIO() - req.write = self.write - self.status_code = 500 - - def _finished(self, arg): - if arg is not None: # pragma: no cover - raise NotImplementedError('cannot handle exceptions yet') - - self.status_code = self._req.code - - self.text = self._io.getvalue() - self.headers = CaseInsensitiveDict((k.lower(), v[-1]) for k, v in self._req.responseHeaders.getAllRawHeaders()) - - def write(self, data): - self._io.write(data) - -class FakeRequests(object): - '''This class wraps a Klein app into a calling interface that is similar - to the requests module for testing apps. - - Example test: - ``` - app = Klein() - - @app.route('/') - def home(request): - return 'hello' - - class TestFakeRequests(unittest.TestCase): - def setUp(self): - self.requests = FakeRequests(app) - - def test_basic(self): - r = self.requests.get('/') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'hello') - ``` - ''' - - def __init__(self, app): - '''Wrap the passed in app as if it was a server for a requests - like interface. The URLs expected will not be complete urls.''' - - self._app = app - self._res = app.resource() - - def _makerequest(self, method, url, data=''): - if url[0:1] != b'/': - raise ValueError('url must be absolute (start w/ a slash)') - - req = FakeHTTPRequest('GET', url, data) - resp = FakeRequestsResponse(req) - - req.notifyFinish().addBoth(resp._finished) - - r = self._res.render(req) - - return resp - - def get(self, url): - '''Return a response for the passed in url.''' - - print('ktg:', repr(url)) - return self._makerequest('GET', url) - - def put(self, url, data=''): - '''Make a put request to the provied URL w/ the body of data.''' - - return self._makerequest('PUT', url, data) - -class TestFakeRequests(unittest.TestCase): - def setUp(self): - self.putdata = [] - - app = Klein() - - @app.route('/') - def home(request): - request.setHeader('x-testing', 'value') - - return b'hello' - - @app.route('/500') - def causeerror(request): - raise ValueError('random exception') - - @app.route('/put') - def putreq(request): - self.putdata.append(request.content.read()) - - request.setResponseCode(201) - return '' - - @app.route('/404') - def notfound(request): - request.setResponseCode(404) - return 'not found' - - self.requests = FakeRequests(app) - - def test_bad(self): - self.assertRaises(ValueError, self.requests.get, b'foobar') - - def test_basic(self): - r = self.requests.get(b'/') - print(repr(r)) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'hello') - self.assertEqual(r.headers['X-testing'], 'value') - - r = self.requests.get('/404') - self.assertEqual(r.status_code, 404) - - r = self.requests.get('/nonexistent') - self.assertEqual(r.status_code, 404) - - r = self.requests.get('/500') - self.assertEqual(r.status_code, 500) - - body = 'body' - r = self.requests.put('/put', data=body) - self.assertEqual(r.status_code, 201) - self.assertEqual(r.text, '') - self.assertEqual(''.join(self.putdata), body) diff --git a/ui/cli.py b/ui/medashare/cli.py similarity index 99% rename from ui/cli.py rename to ui/medashare/cli.py index 4c1ee7a..3c5683e 100644 --- a/ui/cli.py +++ b/ui/medashare/cli.py @@ -12,7 +12,7 @@ import copy import datetime import functools import hashlib -import mock +from unittest import mock import os.path import pasn1 import shutil diff --git a/ui/medashare/config.py b/ui/medashare/config.py new file mode 100644 index 0000000..ea3bb57 --- /dev/null +++ b/ui/medashare/config.py @@ -0,0 +1,11 @@ +from pydantic import BaseSettings, Field + +import asyncio + +__all__ = [ 'Settings' ] + +class Settings(BaseSettings): + db_file: str = Field(description='path to SQLite3 database file') + + class Config: + env_file = '.env' diff --git a/ui/mtree.py b/ui/medashare/mtree.py similarity index 100% rename from ui/mtree.py rename to ui/medashare/mtree.py diff --git a/ui/server.py b/ui/medashare/server.py similarity index 84% rename from ui/server.py rename to ui/medashare/server.py index a1101fd..e9f9ef4 100644 --- a/ui/server.py +++ b/ui/medashare/server.py @@ -2,28 +2,34 @@ # Notes: # Python requests: https://2.python-requests.org/en/master/ -# IRequest interface: https://twistedmatrix.com/documents/current/api/twisted.web.iweb.IRequest.html -# IResource interface: https://twistedmatrix.com/documents/current/api/twisted.web.resource.IResource.html -# Twisted TDD: https://twistedmatrix.com/documents/current/core/howto/trial.html -# Hypothesis: https://hypothesis.readthedocs.io/en/latest/ -# Going Async from Flask to Twisted Klein: https://crossbario.com/blog/Going-Asynchronous-from-Flask-to-Twisted-Klein/ -# Klein POST docs: https://klein.readthedocs.io/en/latest/examples/handlingpost.html - -from klein import Klein -from kleintest import * -from twisted.trial import unittest -from twisted.web.iweb import IRequest -from cli import _asn1coder, Persona, MDBase, MetaData +from .cli import _asn1coder, Persona, MDBase, MetaData + +from fastapi import FastAPI, APIRouter, Depends +from fastapi_restful.cbv import cbv +from functools import lru_cache +from unittest import mock + +from . import config import hashlib -import mock import os.path import shutil import tempfile +import unittest import uuid +router = APIRouter() + +@lru_cache() +def get_settings(): # pragma: cover + return config.Settings() + defaultfile = 'mediaserver.store.pasn1' + +@cbv(router) class MEDAServer: + settings: config.Settings = Depends(get_settings) + def __init__(self, fname): self._fname = fname self._hashes = {} @@ -42,8 +48,6 @@ class MEDAServer: self._trustedkeys = {} self._objstore = {} - app = Klein() - def addpubkey(self, pubkey): persona = Persona.from_pubkey(pubkey) @@ -58,14 +62,14 @@ class MEDAServer: with open(self._fname, 'w') as fp: fp.write(_asn1coder.dumps(obj)) - @app.route('/lookup/') + @router.get('/lookup/') def lookup(self, request, hash): if hash in self._hashes: return request.setResponseCode(404) - @app.route('/obj/') + @router.get('/obj/') def obj_lookup(self, request, id): try: id = uuid.UUID(id) @@ -84,7 +88,7 @@ class MEDAServer: except AttributeError: pass - @app.route('/store') + @router.post('/store') def storeobj(self, request): try: obj = MDBase.decode(request.content.read()) @@ -104,6 +108,12 @@ class MEDAServer: except Exception: request.setResponseCode(401) +def getApp(): + app = FastAPI() + app.include_router(router) + + return app + # twistd support #medaserver = MEDAServer() #resource = medaserver.app.resource @@ -132,17 +142,20 @@ def main(): if __name__ == '__main__': # pragma: no cover main() -class _BaseServerTest(unittest.TestCase): - def setUp(self): +class _BaseServerTest(unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.app = getApp() + + # setup test database + self.dbtempfile = tempfile.NamedTemporaryFile() + + # setup settings + self.settings = config.Settings(db_file=self.dbtempfile.name, + ) + d = os.path.realpath(tempfile.mkdtemp()) self.basetempdir = d self.medaserverfile = os.path.join(self.basetempdir, 'serverstore.pasn1') - self.medaserver = MEDAServer(self.medaserverfile) - self.requests = FakeRequests(self.medaserver.app) - - def tearDown(self): - shutil.rmtree(self.basetempdir) - self.basetempdir = None class _TestCases(_BaseServerTest): def test_objlookup(self): diff --git a/ui/medashare/tests.py b/ui/medashare/tests.py new file mode 100644 index 0000000..daf52d9 --- /dev/null +++ b/ui/medashare/tests.py @@ -0,0 +1,3 @@ +from .cli import _TestCononicalCoder, _TestCases +from .mtree import Test +from .server import _TestCases, _TestPostConfig diff --git a/ui/requirements.txt b/ui/requirements.txt index dab9871..c518e6b 100644 --- a/ui/requirements.txt +++ b/ui/requirements.txt @@ -1,9 +1,4 @@ -urwid --e git+https://www.funkthat.com/gitea/jmg/pasn1.git@01d8efffd7bc3037dcb894ea44dbe959035948c6#egg=pasn1 -coverage -mock -klein -cryptography -base58 -# for kleintest -requests +# use setup.py for dependancy info +-e . + +-e .[dev] diff --git a/ui/setup.py b/ui/setup.py new file mode 100644 index 0000000..5314e06 --- /dev/null +++ b/ui/setup.py @@ -0,0 +1,42 @@ + +# python setup.py --dry-run --verbose install + +import os.path +from setuptools import setup, find_packages + +from distutils.core import setup + +setup( + name='medashare', + version='0.1.0', + author='John-Mark Gurney', + author_email='jmg@funkthat.com', + packages=find_packages(), + #url='', + license='BSD', + description='File Metadata sharing, query and storing utility.', + #download_url='', + long_description=open('README.md').read(), + python_requires='>=3.8', + install_requires=[ + 'base58', + 'cryptography', + 'databases[sqlite]', + 'fastapi', + 'fastapi_restful', + 'httpx', + 'hypercorn', # option, for server only? + 'orm', + 'pasn1 @ git+https://www.funkthat.com/gitea/jmg/pasn1.git@01d8efffd7bc3037dcb894ea44dbe959035948c6#egg=pasn1', + 'pydantic[dotenv]', + ], + extras_require = { + # requests needed for fastpi.testclient.TestClient + 'dev': [ 'coverage', 'requests' ], + }, + entry_points={ + 'console_scripts': [ + 'medashare = medashare.__main__:main', + ] + } +)