| @@ -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 | |||
| @@ -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) | |||
| @@ -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 | |||
| @@ -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' | |||
| @@ -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/<hash>') | |||
| @router.get('/lookup/<hash>') | |||
| def lookup(self, request, hash): | |||
| if hash in self._hashes: | |||
| return | |||
| request.setResponseCode(404) | |||
| @app.route('/obj/<id>') | |||
| @router.get('/obj/<id>') | |||
| 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): | |||
| @@ -0,0 +1,3 @@ | |||
| from .cli import _TestCononicalCoder, _TestCases | |||
| from .mtree import Test | |||
| from .server import _TestCases, _TestPostConfig | |||
| @@ -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] | |||
| @@ -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', | |||
| ] | |||
| } | |||
| ) | |||