one minor issue is that the identity object gets dump'd twice..main
@@ -1,7 +1,7 @@ | |||||
VIRTUALENV?=python3 -m venv | VIRTUALENV?=python3 -m venv | ||||
MODULES=medashare | 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 && \ | (. ./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/\*')) | ((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 && \ | (. ./p/bin/activate && \ | ||||
pip install -r requirements.txt) | 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 ) | (. ./p/bin/activate && cd fixtures && PYTHONPATH=.. python3 genfixtures.py ) | ||||
fixtures/sample.mtree: fixtures/mtree.dir | fixtures/sample.mtree: fixtures/mtree.dir | ||||
@@ -206,7 +206,14 @@ | |||||
"title": "dump is correct", | "title": "dump is correct", | ||||
"cmd": [ "dump" ], | "cmd": [ "dump" ], | ||||
"exit": 0, | "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", | "title": "that import can be done", | ||||
@@ -23,7 +23,7 @@ | |||||
"count": 5 | "count": 5 | ||||
}, | }, | ||||
{ | { | ||||
"title": "verify correct files imported", | |||||
"title": "verify correct files imported a", | |||||
"cmd": [ "dump" ], | "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" | "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" ] | "cmd": [ "container", "somedir.torrent" ] | ||||
}, | }, | ||||
{ | { | ||||
"title": "verify correct files imported", | |||||
"title": "verify correct files imported b", | |||||
"cmd": [ "dump" ], | "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", | "special": "verify store object cnt", | ||||
@@ -37,7 +37,7 @@ | |||||
"title": "that a mapping with unknown host errors", | "title": "that a mapping with unknown host errors", | ||||
"cmd": [ "mapping", "--create", "ceaa4862-dd00-41ba-9787-7480ec1b267a:/foo", "efdb5d9c-d123-4b30-aaa8-45a9ea8f6053:/bar" ], | "cmd": [ "mapping", "--create", "ceaa4862-dd00-41ba-9787-7480ec1b267a:/foo", "efdb5d9c-d123-4b30-aaa8-45a9ea8f6053:/bar" ], | ||||
"exit": 1, | "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", | "title": "that a host mapping that isn't absolute errors", | ||||
@@ -1,12 +1,13 @@ | |||||
import pasn1 | import pasn1 | ||||
import cli | |||||
from medashare import cli | |||||
import datetime | import datetime | ||||
import uuid | import uuid | ||||
persona = cli.Persona() | persona = cli.Persona() | ||||
persona.generate_key() | persona.generate_key() | ||||
cbr = persona.get_identity().uuid | cbr = persona.get_identity().uuid | ||||
objst = cli.ObjectStore(cbr) | |||||
storename = 'sample.data.sqlite3' | |||||
objst = cli.ObjectStore.load(storename, cbr) | |||||
list(map(objst.loadobj, | list(map(objst.loadobj, | ||||
[ | [ | ||||
{ | { | ||||
@@ -21,5 +22,4 @@ list(map(objst.loadobj, | |||||
] | ] | ||||
)) | )) | ||||
objst.store('sample.data.pasn1') | |||||
persona.store('sample.persona.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 import _TestCases as btv_test_cases | ||||
from .btv.bencode import _TestCases as bencode_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 _TestCononicalCoder, _TestCases as cli_test_cases | ||||
from .cli import _TestJSONEncoder | |||||
from .mtree import Test | from .mtree import Test | ||||
from .server import _TestCases, _TestPostConfig | 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', | ||||
'fastapi_restful', | 'fastapi_restful', | ||||
'httpx', | 'httpx', | ||||
'SQLAlchemy', | |||||
'hypercorn', # option, for server only? | 'hypercorn', # option, for server only? | ||||
'orm', | 'orm', | ||||
'pasn1 @ git+https://www.funkthat.com/gitea/jmg/pasn1.git@c6c64510b42292557ace2b77272eb32cb647399d#egg=pasn1', | 'pasn1 @ git+https://www.funkthat.com/gitea/jmg/pasn1.git@c6c64510b42292557ace2b77272eb32cb647399d#egg=pasn1', | ||||