one minor issue is that the identity object gets dump'd twice..main
@@ -1,7 +1,7 @@ | |||
VIRTUALENV?=python3 -m venv | |||
MODULES=medashare | |||
test: fixtures/sample.data.pasn1 fixtures/sample.persona.pasn1 fixtures/sample.mtree | |||
test: fixtures/sample.data.sqlite3 fixtures/sample.persona.pasn1 fixtures/sample.mtree | |||
(. ./p/bin/activate && \ | |||
((find fixtures -type f; find $(MODULES) -type f) | entr sh -c 'python3 -m coverage run -m unittest --failfast $(MODULES).tests && coverage report -m --omit=p/\*')) | |||
@@ -10,7 +10,7 @@ env: | |||
(. ./p/bin/activate && \ | |||
pip install -r requirements.txt) | |||
fixtures/sample.data.pasn1 fixtures/sample.persona.pasn1: fixtures/genfixtures.py | |||
fixtures/sample.data.sqlite3 fixtures/sample.persona.pasn1: fixtures/genfixtures.py | |||
(. ./p/bin/activate && cd fixtures && PYTHONPATH=.. python3 genfixtures.py ) | |||
fixtures/sample.mtree: fixtures/mtree.dir | |||
@@ -206,7 +206,14 @@ | |||
"title": "dump is correct", | |||
"cmd": [ "dump" ], | |||
"exit": 0, | |||
"stdout_re": "{.*name.*Changed Name.*type.*identity.*}\n{.*filename.*newfile.txt.*hashes.*90f8342520f0ac57fb5a779f5d331c2fa87aa40f8799940257f9ba619940951e67143a8d746535ed0284924b2b7bc1478f095198800ba96d01847d7b56ca465c.*size.*19.*type.*file.*}\n{.*foo.*bar=baz.*hashes.*90f8342520f0ac57fb5a779f5d331c2fa87aa40f8799940257f9ba619940951e67143a8d746535ed0284924b2b7bc1478f095198800ba96d01847d7b56ca465c.*type.*metadata.*}\n{.*filename.*test.txt.*90f8342520f0ac57fb5a779f5d331c2fa87aa40f8799940257f9ba619940951e67143a8d746535ed0284924b2b7bc1478f095198800ba96d01847d7b56ca465c.*size.*19.*type.*file.*}\n{.*filename.*newfile.txt.*90f8342520f0ac57fb5a779f5d331c2fa87aa40f8799940257f9ba619940951e67143a8d746535ed0284924b2b7bc1478f095198800ba96d01847d7b56ca465c.*size.*19.*type.*file.*}\n{.*filename.*newfile.txt.*90f8342520f0ac57fb5a779f5d331c2fa87aa40f8799940257f9ba619940951e67143a8d746535ed0284924b2b7bc1478f095198800ba96d01847d7b56ca465c.*size.*19.*type.*file.*}\n" | |||
"stdout_check": [ | |||
{ "name": "Changed Name", "type": "identity" }, | |||
{ "filename": "newfile.txt", "hashes": [ "sha512:90f8342520f0ac57fb5a779f5d331c2fa87aa40f8799940257f9ba619940951e67143a8d746535ed0284924b2b7bc1478f095198800ba96d01847d7b56ca465c" ], "size": 19, "type": "file" }, | |||
{ "foo": [ "bar=baz" ], "hashes": [ "sha512:90f8342520f0ac57fb5a779f5d331c2fa87aa40f8799940257f9ba619940951e67143a8d746535ed0284924b2b7bc1478f095198800ba96d01847d7b56ca465c" ], "type": "metadata" }, | |||
{ "filename": "test.txt", "hashes": [ "sha512:90f8342520f0ac57fb5a779f5d331c2fa87aa40f8799940257f9ba619940951e67143a8d746535ed0284924b2b7bc1478f095198800ba96d01847d7b56ca465c" ], "size": 19, "type": "file" }, | |||
{ "filename": "newfile.txt", "hashes": [ "sha512:90f8342520f0ac57fb5a779f5d331c2fa87aa40f8799940257f9ba619940951e67143a8d746535ed0284924b2b7bc1478f095198800ba96d01847d7b56ca465c" ], "size": 19, "type": "file" }, | |||
{ "filename": "newfile.txt", "hashes": [ "sha512:b0551b2fb5d045a74d36a08ac49aea66790ea4fb5e84f9326a32db44fc78ca0676e65a8d1f0d98f62589eeaef105f303c81f07f3f862ad3bace7960fe59de4d5" ], "size": 17, "type": "file" } | |||
] | |||
}, | |||
{ | |||
"title": "that import can be done", | |||
@@ -23,7 +23,7 @@ | |||
"count": 5 | |||
}, | |||
{ | |||
"title": "verify correct files imported", | |||
"title": "verify correct files imported a", | |||
"cmd": [ "dump" ], | |||
"stdout_re": "fileb.txt.*file.*\n.*foo.*bar.*cc06808cbbee0510331aa97974132e8dc296aeb795be229d064bae784b0a87a5cf4281d82e8c99271b75db2148f08a026c1a60ed9cabdb8cac6d24242dac4063.*\n.*filed.txt.*file.*\n.*filef.txt.*file.*\n.*fileb.txt.*filed.txt.*filef.txt.*cc06808cbbee0510331aa97974132e8dc296aeb795be229d064bae784b0a87a5cf4281d82e8c99271b75db2148f08a026c1.*7831bd05e23877e08a97362bab2ad7bcc7d08d8f841f42e8dee545781792b987aa7637f12cec399e261f798c10d3475add0db7de2643af86a346b6b451a69ec4.*be688838ca8686e5c90689bf2ab585cef1137c.*incomplete.*true.*container.*magnet:\\?xt=urn:btih:501cf3bd4797f49fd7a624e8a9a8ce5cccceb602&dn=somedir" | |||
}, | |||
@@ -45,9 +45,25 @@ | |||
"cmd": [ "container", "somedir.torrent" ] | |||
}, | |||
{ | |||
"title": "verify correct files imported", | |||
"title": "verify correct files imported b", | |||
"cmd": [ "dump" ], | |||
"stdout_re": ".*\n.*fileb.txt.*file.*\n.*foo.*bar.*cc06808cbbee0510331aa97974132e8dc296aeb795be229d064bae784b0a87a5cf4281d82e8c99271b75db2148f08a026c1a60ed9cabdb8cac6d24242dac4063.*\n.*filed.txt.*file.*\n.*filef.txt.*file.*\n.*filea.txt.*fileb.txt.*filec.txt.*filed.txt.*filee.txt.*filef.txt.*0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6.*cc06808cbbee0510331aa97974132e8dc296aeb795be229d064bae784b0a87a5cf4281d82e8c99271b75db2148f08a026c1.*7831bd05e23877e08a97362bab2ad7bcc7d08d8f841f42e8dee545781792b987aa7637f12cec399e261f798c10d3475add0db7de2643af86a346b6b451a69ec4.*be688838ca8686e5c90689bf2ab585cef1137c.*container.*magnet:\\?xt=urn:btih:501cf3bd4797f49fd7a624e8a9a8ce5cccceb602&dn=somedir" | |||
"stdout_check": [ | |||
{ "type": "identity" }, | |||
{ "foo": [ "bar" ], "hashes": [ | |||
"sha512:cc06808cbbee0510331aa97974132e8dc296aeb795be229d064bae784b0a87a5cf4281d82e8c99271b75db2148f08a026c1a60ed9cabdb8cac6d24242dac4063" ] }, | |||
{ "foo": [ "bar" ], "hashes": [ | |||
"sha512:7831bd05e23877e08a97362bab2ad7bcc7d08d8f841f42e8dee545781792b987aa7637f12cec399e261f798c10d3475add0db7de2643af86a346b6b451a69ec4" ] }, | |||
{ "filename": "filea.txt", "type": "file" }, | |||
{ "filename": "fileb.txt", "type": "file" }, | |||
{ "filename": "filec.txt", "type": "file" }, | |||
{ "filename": "filed.txt", "type": "file" }, | |||
{ "filename": "filee.txt", "type": "file" }, | |||
{ "filename": "filef.txt", "type": "file" }, | |||
{ "files": [ "filea.txt", "fileb.txt", "filec.txt", "filed.txt", "filee.txt", "filef/filef.txt" ], | |||
"hashes": [ "sha512:0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6", "sha512:cc06808cbbee0510331aa97974132e8dc296aeb795be229d064bae784b0a87a5cf4281d82e8c99271b75db2148f08a026c1a60ed9cabdb8cac6d24242dac4063", "sha512:cb9eb9ec6c2cd9b0d9451e6b179d91e24906a3123be5e5f18e182be09fab30ad6f5de391bb3cf53933d3a1ca29fdd68d23e17c49fbc1a9117c8ab08154c7df30", "sha512:7831bd05e23877e08a97362bab2ad7bcc7d08d8f841f42e8dee545781792b987aa7637f12cec399e261f798c10d3475add0db7de2643af86a346b6b451a69ec4", "sha512:b09a577f24fb7a6f4b3ea641b2b67120e187b605ef27db97bef178457d9002bec846435a205466e327e5ab151ab1b350b5ac1c9f97e48333cec84fecec3b7037", "sha512:be688838ca8686e5c90689bf2ab585cef1137c999b48c70b92f67a5c34dc15697b5d11c982ed6d71be1e1e7f7b4e0733884aa97c3f7a339a8ed03577cf74be09" ], | |||
"type": "container", | |||
"uri": "magnet:?xt=urn:btih:501cf3bd4797f49fd7a624e8a9a8ce5cccceb602&dn=somedir" } | |||
] | |||
}, | |||
{ | |||
"special": "verify store object cnt", | |||
@@ -37,7 +37,7 @@ | |||
"title": "that a mapping with unknown host errors", | |||
"cmd": [ "mapping", "--create", "ceaa4862-dd00-41ba-9787-7480ec1b267a:/foo", "efdb5d9c-d123-4b30-aaa8-45a9ea8f6053:/bar" ], | |||
"exit": 1, | |||
"stderr": "ERROR: Unable to find host 'ceaa4862-dd00-41ba-9787-7480ec1b267a'\n" | |||
"stderr": "ERROR: Unable to find host ceaa4862-dd00-41ba-9787-7480ec1b267a\n" | |||
}, | |||
{ | |||
"title": "that a host mapping that isn't absolute errors", | |||
@@ -1,12 +1,13 @@ | |||
import pasn1 | |||
import cli | |||
from medashare import cli | |||
import datetime | |||
import uuid | |||
persona = cli.Persona() | |||
persona.generate_key() | |||
cbr = persona.get_identity().uuid | |||
objst = cli.ObjectStore(cbr) | |||
storename = 'sample.data.sqlite3' | |||
objst = cli.ObjectStore.load(storename, cbr) | |||
list(map(objst.loadobj, | |||
[ | |||
{ | |||
@@ -21,5 +22,4 @@ list(map(objst.loadobj, | |||
] | |||
)) | |||
objst.store('sample.data.pasn1') | |||
persona.store('sample.persona.pasn1') |
@@ -0,0 +1,197 @@ | |||
import base64 | |||
import copy | |||
import datetime | |||
import itertools | |||
import json | |||
import unittest | |||
import uuid | |||
from .utils import _makeuuid, _makedatetime, _makebytes, _asn1coder | |||
class _JSONEncoder(json.JSONEncoder): | |||
def default(self, o): | |||
if isinstance(o, uuid.UUID): | |||
return str(o) | |||
elif isinstance(o, datetime.datetime): | |||
o = o.astimezone(datetime.timezone.utc) | |||
return o.strftime('%Y-%m-%dT%H:%M:%S.%fZ') | |||
elif isinstance(o, bytes): | |||
return base64.urlsafe_b64encode(o).decode('US-ASCII') | |||
return json.JSONEncoder.default(self, o) | |||
_jsonencoder = _JSONEncoder() | |||
class _TestJSONEncoder(unittest.TestCase): | |||
def test_defaultfailure(self): | |||
class Foo: | |||
pass | |||
self.assertRaises(TypeError, _jsonencoder.encode, Foo()) | |||
# XXX - add validation | |||
# XXX - how to add singletons | |||
class MDBase(object): | |||
'''This is a simple wrapper that turns a JSON object into a pythonesc | |||
object where attribute accesses work.''' | |||
_type = 'invalid' | |||
_generated_properties = { | |||
'uuid': uuid.uuid4, | |||
'modified': lambda: datetime.datetime.now( | |||
tz=datetime.timezone.utc), | |||
} | |||
# When decoding, the decoded value should be passed to this function | |||
# to get the correct type | |||
_instance_properties = { | |||
'uuid': _makeuuid, | |||
'modified': _makedatetime, | |||
'created_by_ref': _makeuuid, | |||
'parent_refs': lambda x: [ _makeuuid(y) for y in x ], | |||
'sig': _makebytes, | |||
} | |||
# Override on a per subclass basis | |||
_class_instance_properties = { | |||
} | |||
_common_properties = [ 'type', 'created_by_ref' ] # XXX - add lang? | |||
_common_optional = set(('parent_refs', 'sig')) | |||
_common_names = set(_common_properties + list( | |||
_generated_properties.keys())) | |||
_common_names_list = _common_properties + list( | |||
_generated_properties.keys()) | |||
def __init__(self, obj={}, **kwargs): | |||
obj = copy.deepcopy(obj) | |||
obj.update(kwargs) | |||
if self._type == MDBase._type: | |||
raise ValueError('call MDBase.create_obj instead so correct class is used.') | |||
if 'type' in obj and obj['type'] != self._type: | |||
raise ValueError( | |||
'trying to create the wrong type of object, got: %s, expected: %s' % | |||
(repr(obj['type']), repr(self._type))) | |||
if 'type' not in obj: | |||
obj['type'] = self._type | |||
for x in self._common_properties: | |||
if x not in obj: | |||
raise ValueError('common property %s not present' % repr(x)) | |||
for x, fun in itertools.chain( | |||
self._instance_properties.items(), | |||
self._class_instance_properties.items()): | |||
if x in obj: | |||
obj[x] = fun(obj[x]) | |||
for x, fun in self._generated_properties.items(): | |||
if x not in obj: | |||
obj[x] = fun() | |||
self._obj = obj | |||
@classmethod | |||
def create_obj(cls, obj): | |||
'''Using obj as a base, create an instance of MDBase of the | |||
correct type. | |||
If the correct type is not found, a ValueError is raised.''' | |||
if isinstance(obj, cls): | |||
obj = obj._obj | |||
ty = obj['type'] | |||
for i in MDBase.__subclasses__(): | |||
if i._type == ty: | |||
return i(obj) | |||
else: | |||
raise ValueError('Unable to find class for type %s' % | |||
repr(ty)) | |||
def new_version(self, *args, dels=(), replaces=()): | |||
'''For each k, v pair, add the property k as an additional one | |||
(or new one if first), with the value v. | |||
Any key in dels is removed. | |||
Any k, v pair in replaces, replaces the entire key.''' | |||
obj = copy.deepcopy(self._obj) | |||
common = self._common_names | self._common_optional | |||
uniquify = set() | |||
for k, v in args: | |||
if k in common: | |||
obj[k] = v | |||
else: | |||
uniquify.add(k) | |||
obj.setdefault(k, []).append(v) | |||
for k in uniquify: | |||
obj[k] = list(set(obj[k])) | |||
for i in dels: | |||
del obj[i] | |||
for k, v in replaces: | |||
obj[k] = v | |||
del obj['modified'] | |||
return self.create_obj(obj) | |||
def __repr__(self): # pragma: no cover | |||
return '%s(%s)' % (self.__class__.__name__, repr(self._obj)) | |||
def __getattr__(self, k): | |||
try: | |||
return self._obj[k] | |||
except KeyError: | |||
raise AttributeError(k) | |||
def __setattr__(self, k, v): | |||
if k[0] == '_': # direct attribute | |||
self.__dict__[k] = v | |||
else: | |||
self._obj[k] = v | |||
def __getitem__(self, k): | |||
return self._obj[k] | |||
def __to_dict__(self): | |||
'''Returns an internal object. If modification is necessary, | |||
make sure to .copy() it first.''' | |||
return self._obj | |||
def __eq__(self, o): | |||
return self._obj == o | |||
def __contains__(self, k): | |||
return k in self._obj | |||
def items(self, skipcommon=True): | |||
return [ (k, v) for k, v in self._obj.items() if | |||
not skipcommon or k not in self._common_names ] | |||
def encode(self, meth='asn1'): | |||
if meth == 'asn1': | |||
return _asn1coder.dumps(self) | |||
return _jsonencoder.encode(self._obj) | |||
@classmethod | |||
def decode(cls, s, meth='asn1'): | |||
if meth == 'asn1': | |||
obj = _asn1coder.loads(s) | |||
else: | |||
obj = json.loads(s) | |||
return cls.create_obj(obj) | |||
@@ -0,0 +1,72 @@ | |||
import uuid | |||
from sqlalchemy import Table, Column, DateTime, String, Integer, LargeBinary | |||
from sqlalchemy import types | |||
from sqlalchemy.orm import declarative_base | |||
from .cli import _debprint | |||
from .mdb import MDBase | |||
Base = declarative_base() | |||
class MDBaseType(types.TypeDecorator): | |||
impl = LargeBinary | |||
cache_ok = True | |||
def process_bind_param(self, value, dialect): | |||
return value.encode() | |||
def process_result_value(self, value, dialect): | |||
return MDBase.decode(value) | |||
class UUID(types.TypeDecorator): | |||
impl = String(32) | |||
cache_ok = True | |||
def process_bind_param(self, value, dialect): | |||
return value.hex | |||
def process_result_value(self, value, dialect): | |||
return uuid.UUID(hex=value) | |||
class Dummy(Base): | |||
__tablename__ = 'dummy' | |||
id = Column(Integer, primary_key=True) | |||
class UUIDv5Table(Base): | |||
__tablename__ = 'uuidv5_index' | |||
uuid = Column(UUID, primary_key=True) | |||
objid = Column(UUID) | |||
class HostMapping(Base): | |||
__tablename__ = 'hostmapping' | |||
hostid = Column(UUID, primary_key=True) | |||
objid = Column(UUID, primary_key=True) | |||
# https://stackoverflow.com/questions/57685385/how-to-avoid-inserting-duplicate-data-when-inserting-data-into-sqlite3-database | |||
#UniqueConstraint('hostid', 'objid', on conflict ignore) | |||
class HostTable(Base): | |||
__tablename__ = 'hosttable' | |||
hostid = Column(UUID, primary_key=True) | |||
objid = Column(UUID) | |||
class HashTable(Base): | |||
__tablename__ = 'hash_index' | |||
hash = Column(String, primary_key=True) | |||
uuid = Column(UUID, primary_key=True) | |||
class MetaDataObject(Base): | |||
__tablename__ = 'metadata_objects' | |||
uuid = Column(UUID, primary_key=True) | |||
modified = Column(DateTime) | |||
data = Column(MDBaseType) | |||
def __repr__(self): | |||
return 'MetaDataObject(uuid=%s, modified=%s, data=%s)' % (repr(self.uuid), repr(self.modified), repr(self.data)) |
@@ -1,6 +1,6 @@ | |||
from .btv import _TestCases as btv_test_cases | |||
from .btv.bencode import _TestCases as bencode_test_cases | |||
from .mdb import _TestJSONEncoder | |||
from .cli import _TestCononicalCoder, _TestCases as cli_test_cases | |||
from .cli import _TestJSONEncoder | |||
from .mtree import Test | |||
from .server import _TestCases, _TestPostConfig |
@@ -0,0 +1,47 @@ | |||
import base64 | |||
import datetime | |||
import pasn1 | |||
import uuid | |||
def _makeuuid(s): | |||
if isinstance(s, uuid.UUID): | |||
return s | |||
if isinstance(s, bytes): | |||
return uuid.UUID(bytes=s) | |||
else: | |||
return uuid.UUID(s) | |||
def _makedatetime(s): | |||
if isinstance(s, datetime.datetime): | |||
return s | |||
return datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%fZ').replace( | |||
tzinfo=datetime.timezone.utc) | |||
def _makebytes(s): | |||
if isinstance(s, bytes): | |||
return s | |||
return base64.urlsafe_b64decode(s) | |||
def _trytodict(o): | |||
if isinstance(o, uuid.UUID): | |||
return 'bytes', o.bytes | |||
if isinstance(o, tuple): | |||
return 'list', o | |||
try: | |||
return 'dict', o.__to_dict__() | |||
except Exception: # pragma: no cover | |||
raise TypeError('unable to find __to_dict__ on %s: %s' % | |||
(type(o), repr(o))) | |||
class CanonicalCoder(pasn1.ASN1DictCoder): | |||
def enc_dict(self, obj, **kwargs): | |||
class FakeIter: | |||
def items(self): | |||
return iter(sorted(obj.items())) | |||
return pasn1.ASN1DictCoder.enc_dict(self, FakeIter(), **kwargs) | |||
_asn1coder = CanonicalCoder(coerce=_trytodict) |
@@ -25,6 +25,7 @@ setup( | |||
'fastapi', | |||
'fastapi_restful', | |||
'httpx', | |||
'SQLAlchemy', | |||
'hypercorn', # option, for server only? | |||
'orm', | |||
'pasn1 @ git+https://www.funkthat.com/gitea/jmg/pasn1.git@c6c64510b42292557ace2b77272eb32cb647399d#egg=pasn1', | |||