Merge pull request #187 from lbryio/new-metadata-system

Rewrite of migration and validation system using JSON Schema
This commit is contained in:
Job Evers‐Meltzer 2016-10-14 15:51:49 -05:00 committed by GitHub
commit 018d78be6f
13 changed files with 421 additions and 350 deletions

View file

@ -13,6 +13,7 @@ from twisted.python.failure import Failure
from twisted.enterprise import adbapi from twisted.enterprise import adbapi
from collections import defaultdict, deque from collections import defaultdict, deque
from zope.interface import implements from zope.interface import implements
from jsonschema import ValidationError
from decimal import Decimal from decimal import Decimal
from lbryum import SimpleConfig, Network from lbryum import SimpleConfig, Network
@ -338,7 +339,7 @@ class Wallet(object):
try: try:
metadata = Metadata(json.loads(result['value'])) metadata = Metadata(json.loads(result['value']))
except (ValueError, TypeError): except ValidationError:
return Failure(InvalidStreamInfoError(name)) return Failure(InvalidStreamInfoError(name))
txid = result['txid'] txid = result['txid']
@ -421,7 +422,7 @@ class Wallet(object):
meta_ver = metadata.version meta_ver = metadata.version
sd_hash = metadata['sources']['lbry_sd_hash'] sd_hash = metadata['sources']['lbry_sd_hash']
d = self._save_name_metadata(name, txid, sd_hash) d = self._save_name_metadata(name, txid, sd_hash)
except AssertionError: except ValidationError:
metadata = claim['value'] metadata = claim['value']
meta_ver = "Non-compliant" meta_ver = "Non-compliant"
d = defer.succeed(None) d = defer.succeed(None)

View file

@ -23,6 +23,7 @@ from twisted.internet.task import LoopingCall
from txjsonrpc import jsonrpclib from txjsonrpc import jsonrpclib
from txjsonrpc.web import jsonrpc from txjsonrpc.web import jsonrpc
from txjsonrpc.web.jsonrpc import Handler from txjsonrpc.web.jsonrpc import Handler
from jsonschema import ValidationError
from lbrynet import __version__ as lbrynet_version from lbrynet import __version__ as lbrynet_version
from lbryum.version import LBRYUM_VERSION as lbryum_version from lbryum.version import LBRYUM_VERSION as lbryum_version
@ -2009,7 +2010,7 @@ class Daemon(jsonrpc.JSONRPC):
metadata = Metadata(p['metadata']) metadata = Metadata(p['metadata'])
make_lbry_file = False make_lbry_file = False
sd_hash = metadata['sources']['lbry_sd_hash'] sd_hash = metadata['sources']['lbry_sd_hash']
except AssertionError: except ValidationError:
make_lbry_file = True make_lbry_file = True
sd_hash = None sd_hash = None
metadata = p['metadata'] metadata = p['metadata']

View file

@ -160,4 +160,4 @@ class Publisher(object):
def get_content_type(filename): def get_content_type(filename):
return mimetypes.guess_type(filename)[0] return mimetypes.guess_type(filename)[0] or 'application/octet-stream'

View file

@ -1,116 +1,39 @@
import logging import logging
import fee_schemas
from lbrynet.metadata.Validator import Validator, skip_validate from lbrynet.metadata.StructuredDict import StructuredDict
from lbrynet.conf import CURRENCIES
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def verify_supported_currency(fee): class FeeValidator(StructuredDict):
assert len(fee) == 1
for c in fee:
assert c in CURRENCIES
return True
def verify_amount(x):
return isinstance(x, float) or isinstance(x, int) and x > 0
class LBCFeeValidator(Validator):
FV001 = "0.0.1"
CURRENT_FEE_VERSION = FV001
FEE_REVISIONS = {}
FEE_REVISIONS[FV001] = [
(Validator.REQUIRE, 'amount', verify_amount),
(Validator.REQUIRE, 'address', skip_validate),
]
FEE_MIGRATIONS = []
current_version = CURRENT_FEE_VERSION
versions = FEE_REVISIONS
migrations = FEE_MIGRATIONS
def __init__(self, fee): def __init__(self, fee):
Validator.__init__(self, fee) self._versions = [
('0.0.1', fee_schemas.VER_001, None)
class BTCFeeValidator(Validator):
FV001 = "0.0.1"
CURRENT_FEE_VERSION = FV001
FEE_REVISIONS = {}
FEE_REVISIONS[FV001] = [
(Validator.REQUIRE, 'amount',verify_amount),
(Validator.REQUIRE, 'address', skip_validate),
] ]
FEE_MIGRATIONS = [] StructuredDict.__init__(self, fee, fee.get('ver', '0.0.1'))
current_version = CURRENT_FEE_VERSION
versions = FEE_REVISIONS
migrations = FEE_MIGRATIONS
def __init__(self, fee):
Validator.__init__(self, fee)
class USDFeeValidator(Validator):
FV001 = "0.0.1"
CURRENT_FEE_VERSION = FV001
FEE_REVISIONS = {}
FEE_REVISIONS[FV001] = [
(Validator.REQUIRE, 'amount',verify_amount),
(Validator.REQUIRE, 'address', skip_validate),
]
FEE_MIGRATIONS = []
current_version = CURRENT_FEE_VERSION
versions = FEE_REVISIONS
migrations = FEE_MIGRATIONS
def __init__(self, fee):
Validator.__init__(self, fee)
class FeeValidator(Validator):
CV001 = "0.0.1"
CURRENT_CURRENCY_VERSION = CV001
CURRENCY_REVISIONS = {}
CURRENCY_REVISIONS[CV001] = [
(Validator.OPTIONAL, 'BTC', BTCFeeValidator.validate),
(Validator.OPTIONAL, 'USD', USDFeeValidator.validate),
(Validator.OPTIONAL, 'LBC', LBCFeeValidator.validate),
]
CURRENCY_MIGRATIONS = []
current_version = CURRENT_CURRENCY_VERSION
versions = CURRENCY_REVISIONS
migrations = CURRENCY_MIGRATIONS
def __init__(self, fee_dict):
Validator.__init__(self, fee_dict)
self.currency_symbol = self.keys()[0] self.currency_symbol = self.keys()[0]
self.amount = self._get_amount() self.amount = self._get_amount()
self.address = self[self.currency_symbol]['address'] self.address = self[self.currency_symbol]['address']
def _get_amount(self): def _get_amount(self):
amt = self[self.currency_symbol]['amount'] amt = self[self.currency_symbol]['amount']
if isinstance(amt, float):
return amt
else:
try: try:
return float(amt) return float(amt)
except TypeError: except TypeError:
log.error('Failed to convert %s to float', amt) log.error('Failed to convert fee amount %s to float', amt)
raise raise
class LBCFeeValidator(StructuredDict):
pass
class BTCFeeValidator(StructuredDict):
pass
class USDFeeValidator(StructuredDict):
pass

View file

@ -1,8 +1,7 @@
import logging import logging
from lbrynet.metadata.Validator import Validator, skip_validate from lbrynet.metadata.StructuredDict import StructuredDict
from lbrynet.metadata.Fee import FeeValidator, verify_supported_currency import metadata_schemas
from lbrynet.conf import SOURCE_TYPES
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
NAME_ALLOWED_CHARSET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0987654321-' NAME_ALLOWED_CHARSET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0987654321-'
@ -13,74 +12,27 @@ def verify_name_characters(name):
assert c in NAME_ALLOWED_CHARSET, "Invalid character" assert c in NAME_ALLOWED_CHARSET, "Invalid character"
return True return True
def migrate_001_to_002(metadata):
metadata['ver'] = '0.0.2'
metadata['nsfw'] = False
def validate_sources(sources): def migrate_002_to_003(metadata):
for source in sources: metadata['ver'] = '0.0.3'
assert source in SOURCE_TYPES, "Unknown source type: %s" % str(source) if 'content-type' in metadata:
return True metadata['content_type'] = metadata['content-type']
del metadata['content-type']
class Metadata(Validator): class Metadata(StructuredDict):
MV001 = "0.0.1" current_version = '0.0.3'
MV002 = "0.0.2"
MV003 = "0.0.3"
CURRENT_METADATA_VERSION = MV003
METADATA_REVISIONS = {} _versions = [
('0.0.1', metadata_schemas.VER_001, None),
METADATA_REVISIONS[MV001] = [ ('0.0.2', metadata_schemas.VER_002, migrate_001_to_002),
(Validator.REQUIRE, 'title', skip_validate), ('0.0.3', metadata_schemas.VER_003, migrate_002_to_003)
(Validator.REQUIRE, 'description', skip_validate),
(Validator.REQUIRE, 'author', skip_validate),
(Validator.REQUIRE, 'language', skip_validate),
(Validator.REQUIRE, 'license', skip_validate),
(Validator.REQUIRE, 'content-type', skip_validate),
(Validator.REQUIRE, 'sources', validate_sources),
(Validator.OPTIONAL, 'thumbnail', skip_validate),
(Validator.OPTIONAL, 'preview', skip_validate),
(Validator.OPTIONAL, 'fee', verify_supported_currency),
(Validator.OPTIONAL, 'contact', skip_validate),
(Validator.OPTIONAL, 'pubkey', skip_validate),
] ]
METADATA_REVISIONS[MV002] = [ def __init__(self, metadata, migrate=True, target_version=None):
(Validator.REQUIRE, 'nsfw', skip_validate), starting_version = metadata.get('ver', '0.0.1')
(Validator.REQUIRE, 'ver', skip_validate),
(Validator.OPTIONAL, 'license_url', skip_validate),
]
METADATA_REVISIONS[MV003] = [ StructuredDict.__init__(self, metadata, starting_version, migrate, target_version)
(Validator.REQUIRE, 'content_type', skip_validate),
(Validator.SKIP, 'content-type'),
(Validator.OPTIONAL, 'sig', skip_validate),
(Validator.IF_KEY, 'sig', (Validator.REQUIRE, 'pubkey', skip_validate), Validator.DO_NOTHING),
(Validator.IF_KEY, 'pubkey', (Validator.REQUIRE, 'sig', skip_validate), Validator.DO_NOTHING),
]
MIGRATE_MV001_TO_MV002 = [
(Validator.IF_KEY, 'nsfw', Validator.DO_NOTHING, (Validator.LOAD, 'nsfw', False)),
(Validator.IF_KEY, 'ver', Validator.DO_NOTHING, (Validator.LOAD, 'ver', MV002)),
]
MIGRATE_MV002_TO_MV003 = [
(Validator.IF_KEY, 'content-type', (Validator.UPDATE, 'content-type', 'content_type'), Validator.DO_NOTHING),
(Validator.IF_VAL, 'ver', MV002, (Validator.LOAD, 'ver', MV003), Validator.DO_NOTHING),
]
METADATA_MIGRATIONS = [
MIGRATE_MV001_TO_MV002,
MIGRATE_MV002_TO_MV003,
]
current_version = CURRENT_METADATA_VERSION
versions = METADATA_REVISIONS
migrations = METADATA_MIGRATIONS
def __init__(self, metadata, process_now=True):
Validator.__init__(self, metadata, process_now)
self.meta_version = self.get('ver', Metadata.MV001)
self._load_fee()
def _load_fee(self):
if 'fee' in self:
self.update({'fee': FeeValidator(self['fee'])})

View file

@ -0,0 +1,62 @@
import jsonschema
import logging
from jsonschema import ValidationError
log = logging.getLogger(__name__)
class StructuredDict(dict):
"""
A dictionary that enforces a structure specified by a schema, and supports
migration between different versions of the schema.
"""
# To be specified in sub-classes, an array in the format
# [(version, schema, migration), ...]
_versions = []
# Used internally to allow schema lookups by version number
_schemas = {}
version = None
def __init__(self, value, starting_version, migrate=True, target_version=None):
dict.__init__(self, value)
self.version = starting_version
self._schemas = dict([(version, schema) for (version, schema, _) in self._versions])
self.validate(starting_version)
if migrate:
self.migrate(target_version)
def _upgrade_version_range(self, start_version, end_version):
after_starting_version = False
for version, schema, migration in self._versions:
if not after_starting_version:
if version == self.version:
after_starting_version = True
continue
yield version, schema, migration
if end_version and version == end_version:
break
def validate(self, version):
jsonschema.validate(self, self._schemas[version])
def migrate(self, target_version=None):
if target_version:
assert self._versions.index(target_version) > self.versions.index(self.version), "Current version is above target version"
for version, schema, migration in self._upgrade_version_range(self.version, target_version):
migration(self)
try:
self.validate(version)
except ValidationError as e:
raise ValidationError, "Could not migrate to version %s due to validation error: %s" % (version, e.message)
self.version = version

View file

@ -1,155 +0,0 @@
import json
import logging
from copy import deepcopy
from distutils.version import StrictVersion
from lbrynet.core.utils import version_is_greater_than
log = logging.getLogger(__name__)
def skip_validate(value):
return True
def processor(cls):
for methodname in dir(cls):
method = getattr(cls, methodname)
if hasattr(method, 'cmd_name'):
cls.commands.update({method.cmd_name: methodname})
return cls
def cmd(cmd_name):
def wrapper(func):
func.cmd_name = cmd_name
return func
return wrapper
@processor
class Validator(dict):
"""
Base class for validated dictionaries
"""
# override these
current_version = None
versions = {}
migrations = []
# built in commands
DO_NOTHING = "do_nothing"
UPDATE = "update_key"
IF_KEY = "if_key"
REQUIRE = "require"
SKIP = "skip"
OPTIONAL = "optional"
LOAD = "load"
IF_VAL = "if_val"
commands = {}
@classmethod
def load_from_hex(cls, hex_val):
return cls(json.loads(hex_val.decode('hex')))
@classmethod
def validate(cls, value):
if cls(value):
return True
else:
return False
def __init__(self, value, process_now=False):
dict.__init__(self)
self._skip = []
value_to_load = deepcopy(value)
if process_now:
self.process(value_to_load)
self._verify_value(value_to_load)
self.version = self.get('ver', "0.0.1")
def process(self, value):
if self.migrations is not None:
self._migrate_value(value)
@cmd(DO_NOTHING)
def _do_nothing(self):
pass
@cmd(SKIP)
def _add_to_skipped(self, rx_value, key):
if key not in self._skip:
self._skip.append(key)
@cmd(UPDATE)
def _update(self, rx_value, old_key, new_key):
rx_value.update({new_key: rx_value.pop(old_key)})
@cmd(IF_KEY)
def _if_key(self, rx_value, key, if_true, if_else):
if key in rx_value:
return self._handle(if_true, rx_value)
return self._handle(if_else, rx_value)
@cmd(IF_VAL)
def _if_val(self, rx_value, key, val, if_true, if_else):
if key in rx_value:
if rx_value[key] == val:
return self._handle(if_true, rx_value)
return self._handle(if_else, rx_value)
@cmd(LOAD)
def _load(self, rx_value, key, value):
rx_value.update({key: value})
@cmd(REQUIRE)
def _require(self, rx_value, key, validator=None):
if key not in self._skip:
assert key in rx_value, "Key is missing: %s" % key
if isinstance(validator, type):
assert isinstance(rx_value[key], validator), "%s: %s isn't required %s" % (key, type(rx_value[key]), validator)
elif callable(validator):
assert validator(rx_value[key]), "Failed to validate %s" % key
self.update({key: rx_value.pop(key)})
@cmd(OPTIONAL)
def _optional(self, rx_value, key, validator=None):
if key in rx_value and key not in self._skip:
if isinstance(validator, type):
assert isinstance(rx_value[key], validator), "%s type %s isn't required %s" % (key, type(rx_value[key]), validator)
elif callable(validator):
assert validator(rx_value[key]), "Failed to validate %s" % key
self.update({key: rx_value.pop(key)})
def _handle(self, cmd_tpl, value):
if cmd_tpl == Validator.DO_NOTHING:
return
command = cmd_tpl[0]
f = getattr(self, self.commands[command])
if len(cmd_tpl) > 1:
args = (value,) + cmd_tpl[1:]
f(*args)
else:
f()
def _load_revision(self, version, value):
for k in self.versions[version]:
self._handle(k, value)
def _verify_value(self, value):
val_ver = value.get('ver', "0.0.1")
# verify version requirements in reverse order starting from the version asserted in the value
versions = sorted([v for v in self.versions if not version_is_greater_than(v, val_ver)], key=StrictVersion, reverse=True)
for version in versions:
self._load_revision(version, value)
assert value == {} or value.keys() == self._skip, "Unknown keys: %s" % json.dumps(value)
def _migrate_value(self, value):
for migration in self.migrations:
self._run_migration(migration, value)
def _run_migration(self, commands, value):
for cmd in commands:
self._handle(cmd, value)

View file

@ -0,0 +1,16 @@
VER_001 = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'title': 'LBRY fee schema 0.0.1',
'type': 'object',
'properties': {
'amount': {
'type': 'number',
'minimum': 0,
'exclusiveMinimum': True,
},
'address': {
'type': 'string'
}
},
}

View file

@ -0,0 +1,269 @@
VER_001 = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'title': 'LBRY metadata schema 0.0.1',
'definitions': {
'fee_info': {
'type': 'object',
'properties': {
'amount': {
'type': 'number',
'minimum': 0,
'exclusiveMinimum': True,
},
'address': {
'type': 'string'
}
},
}
},
'type': 'object',
'properties': {
'ver': {
'type': 'string',
'default': '0.0.1'
},
'title': {
'type': 'string'
},
'description': {
'type': 'string'
},
'author': {
'type': 'string'
},
'language': {
'type': 'string'
},
'license': {
'type': 'string'
},
'content-type': {
'type': 'string'
},
'sources': {
'type': 'object',
'properties': {
'lbry_sd_hash': {
'type': 'string'
},
'btih': {
'type': 'string'
},
'url': {
'type': 'string'
}
},
'additionalProperties': False
},
'thumbnail': {
'type': 'string'
},
'preview': {
'type': 'string'
},
'fee': {
'properties': {
'LBC': { '$ref': '#/definitions/fee_info' },
'BTC': { '$ref': '#/definitions/fee_info' },
'USD': { '$ref': '#/definitions/fee_info' }
}
},
'contact': {
'type': 'number'
},
'pubkey': {
'type': 'string'
},
},
'required': ['title', 'description', 'author', 'language', 'license', 'content-type', 'sources'],
'additionalProperties': False
}
VER_002 = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'title': 'LBRY metadata schema 0.0.2',
'definitions': {
'fee_info': {
'type': 'object',
'properties': {
'amount': {
'type': 'number',
'minimum': 0,
'exclusiveMinimum': True,
},
'address': {
'type': 'string'
}
},
}
},
'type': 'object',
'properties': {
'ver': {
'type': 'string',
'enum': ['0.0.2'],
},
'title': {
'type': 'string'
},
'description': {
'type': 'string'
},
'author': {
'type': 'string'
},
'language': {
'type': 'string'
},
'license': {
'type': 'string'
},
'content-type': {
'type': 'string'
},
'sources': {
'type': 'object',
'properties': {
'lbry_sd_hash': {
'type': 'string'
},
'btih': {
'type': 'string'
},
'url': {
'type': 'string'
}
},
'additionalProperties': False
},
'thumbnail': {
'type': 'string'
},
'preview': {
'type': 'string'
},
'fee': {
'properties': {
'LBC': { '$ref': '#/definitions/fee_info' },
'BTC': { '$ref': '#/definitions/fee_info' },
'USD': { '$ref': '#/definitions/fee_info' }
}
},
'contact': {
'type': 'number'
},
'pubkey': {
'type': 'string'
},
'license_url': {
'type': 'string'
},
'nsfw': {
'type': 'boolean',
'default': False
},
},
'required': ['ver', 'title', 'description', 'author', 'language', 'license', 'content-type', 'sources', 'nsfw'],
'additionalProperties': False
}
VER_003 = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'title': 'LBRY metadata schema 0.0.3',
'definitions': {
'fee_info': {
'type': 'object',
'properties': {
'amount': {
'type': 'number',
'minimum': 0,
'exclusiveMinimum': True,
},
'address': {
'type': 'string'
}
},
}
},
'type': 'object',
'properties': {
'ver': {
'type': 'string',
'enum': ['0.0.3'],
},
'title': {
'type': 'string'
},
'description': {
'type': 'string'
},
'author': {
'type': 'string'
},
'language': {
'type': 'string'
},
'license': {
'type': 'string'
},
'content_type': {
'type': 'string'
},
'sources': {
'type': 'object',
'properties': {
'lbry_sd_hash': {
'type': 'string'
},
'btih': {
'type': 'string'
},
'url': {
'type': 'string'
}
},
'additionalProperties': False
},
'thumbnail': {
'type': 'string'
},
'preview': {
'type': 'string'
},
'fee': {
'properties': {
'LBC': { '$ref': '#/definitions/fee_info' },
'BTC': { '$ref': '#/definitions/fee_info' },
'USD': { '$ref': '#/definitions/fee_info' }
}
},
'contact': {
'type': 'number'
},
'pubkey': {
'type': 'string'
},
'license_url': {
'type': 'string'
},
'nsfw': {
'type': 'boolean',
'default': False
},
'sig': {
'type': 'string'
}
},
'required': ['ver', 'title', 'description', 'author', 'language', 'license', 'content_type', 'sources', 'nsfw'],
'additionalProperties': False,
'dependencies': {
'pubkey': ['sig'],
'sig': ['pubkey']
}
}

View file

@ -8,6 +8,7 @@ ecdsa==0.13
gmpy==1.17 gmpy==1.17
jsonrpc==1.2 jsonrpc==1.2
jsonrpclib==0.1.7 jsonrpclib==0.1.7
jsonschema==2.5.1
https://github.com/lbryio/lbryum/tarball/master/#egg=lbryum https://github.com/lbryio/lbryum/tarball/master/#egg=lbryum
loggly-python-handler==1.0.0 loggly-python-handler==1.0.0
miniupnpc==1.9 miniupnpc==1.9

View file

@ -40,6 +40,7 @@ requires = [
'lbryum', 'lbryum',
'jsonrpc', 'jsonrpc',
'simplejson', 'simplejson',
'jsonschema',
'appdirs', 'appdirs',
'six==1.9.0', 'six==1.9.0',
'base58', 'base58',

View file

@ -1,5 +1,5 @@
import mock import mock
from lbrynet.metadata import Metadata from lbrynet.metadata import Fee
from lbrynet.lbrynet_daemon import ExchangeRateManager from lbrynet.lbrynet_daemon import ExchangeRateManager
from twisted.trial import unittest from twisted.trial import unittest
@ -13,7 +13,7 @@ class FeeFormatTest(unittest.TestCase):
'address': "bRcHraa8bYJZL7vkh5sNmGwPDERFUjGPP9" 'address': "bRcHraa8bYJZL7vkh5sNmGwPDERFUjGPP9"
} }
} }
fee = Metadata.FeeValidator(fee_dict) fee = Fee.FeeValidator(fee_dict)
self.assertEqual(10.0, fee['USD']['amount']) self.assertEqual(10.0, fee['USD']['amount'])

View file

@ -1,14 +1,14 @@
from lbrynet.metadata import Metadata from lbrynet.metadata import Metadata
from twisted.trial import unittest from twisted.trial import unittest
from jsonschema import ValidationError
class MetadataTest(unittest.TestCase): class MetadataTest(unittest.TestCase):
def test_assertion_if_no_metadata(self): def test_validation_error_if_no_metadata(self):
metadata = {} metadata = {}
with self.assertRaises(AssertionError): with self.assertRaises(ValidationError):
Metadata.Metadata(metadata) Metadata.Metadata(metadata)
def test_assertion_if_source_is_missing(self): def test_validation_error_if_source_is_missing(self):
metadata = { metadata = {
'license': 'Oscilloscope Laboratories', 'license': 'Oscilloscope Laboratories',
'description': 'Four couples meet for Sunday brunch only to discover they are stuck in a house together as the world may be about to end.', 'description': 'Four couples meet for Sunday brunch only to discover they are stuck in a house together as the world may be about to end.',
@ -18,7 +18,7 @@ class MetadataTest(unittest.TestCase):
'content-type': 'audio/mpeg', 'content-type': 'audio/mpeg',
'thumbnail': 'http://ia.media-imdb.com/images/M/MV5BMTQwNjYzMTQ0Ml5BMl5BanBnXkFtZTcwNDUzODM5Nw@@._V1_SY1000_CR0,0,673,1000_AL_.jpg', 'thumbnail': 'http://ia.media-imdb.com/images/M/MV5BMTQwNjYzMTQ0Ml5BMl5BanBnXkFtZTcwNDUzODM5Nw@@._V1_SY1000_CR0,0,673,1000_AL_.jpg',
} }
with self.assertRaises(AssertionError): with self.assertRaises(ValidationError):
Metadata.Metadata(metadata) Metadata.Metadata(metadata)
def test_metadata_works_without_fee(self): def test_metadata_works_without_fee(self):
@ -36,7 +36,7 @@ class MetadataTest(unittest.TestCase):
m = Metadata.Metadata(metadata) m = Metadata.Metadata(metadata)
self.assertFalse('fee' in m) self.assertFalse('fee' in m)
def test_assertion_if_invalid_source(self): def test_validation_error_if_invalid_source(self):
metadata = { metadata = {
'license': 'Oscilloscope Laboratories', 'license': 'Oscilloscope Laboratories',
'description': 'Four couples meet for Sunday brunch only to discover they are stuck in a house together as the world may be about to end.', 'description': 'Four couples meet for Sunday brunch only to discover they are stuck in a house together as the world may be about to end.',
@ -48,10 +48,10 @@ class MetadataTest(unittest.TestCase):
'content-type': 'audio/mpeg', 'content-type': 'audio/mpeg',
'thumbnail': 'http://ia.media-imdb.com/images/M/MV5BMTQwNjYzMTQ0Ml5BMl5BanBnXkFtZTcwNDUzODM5Nw@@._V1_SY1000_CR0,0,673,1000_AL_.jpg', 'thumbnail': 'http://ia.media-imdb.com/images/M/MV5BMTQwNjYzMTQ0Ml5BMl5BanBnXkFtZTcwNDUzODM5Nw@@._V1_SY1000_CR0,0,673,1000_AL_.jpg',
} }
with self.assertRaises(AssertionError): with self.assertRaises(ValidationError):
Metadata.Metadata(metadata) Metadata.Metadata(metadata)
def test_assertion_if_missing_v001_field(self): def test_validation_error_if_missing_v001_field(self):
metadata = { metadata = {
'license': 'Oscilloscope Laboratories', 'license': 'Oscilloscope Laboratories',
'fee': {'LBC': {'amount': 50.0, 'address': 'bRQJASJrDbFZVAvcpv3NoNWoH74LQd5JNV'}}, 'fee': {'LBC': {'amount': 50.0, 'address': 'bRQJASJrDbFZVAvcpv3NoNWoH74LQd5JNV'}},
@ -63,7 +63,7 @@ class MetadataTest(unittest.TestCase):
'content-type': 'audio/mpeg', 'content-type': 'audio/mpeg',
'thumbnail': 'http://ia.media-imdb.com/images/M/MV5BMTQwNjYzMTQ0Ml5BMl5BanBnXkFtZTcwNDUzODM5Nw@@._V1_SY1000_CR0,0,673,1000_AL_.jpg' 'thumbnail': 'http://ia.media-imdb.com/images/M/MV5BMTQwNjYzMTQ0Ml5BMl5BanBnXkFtZTcwNDUzODM5Nw@@._V1_SY1000_CR0,0,673,1000_AL_.jpg'
} }
with self.assertRaises(AssertionError): with self.assertRaises(ValidationError):
Metadata.Metadata(metadata) Metadata.Metadata(metadata)
def test_version_is_001_if_all_fields_are_present(self): def test_version_is_001_if_all_fields_are_present(self):
@ -78,10 +78,10 @@ class MetadataTest(unittest.TestCase):
'content-type': 'audio/mpeg', 'content-type': 'audio/mpeg',
'thumbnail': 'http://ia.media-imdb.com/images/M/MV5BMTQwNjYzMTQ0Ml5BMl5BanBnXkFtZTcwNDUzODM5Nw@@._V1_SY1000_CR0,0,673,1000_AL_.jpg', 'thumbnail': 'http://ia.media-imdb.com/images/M/MV5BMTQwNjYzMTQ0Ml5BMl5BanBnXkFtZTcwNDUzODM5Nw@@._V1_SY1000_CR0,0,673,1000_AL_.jpg',
} }
m = Metadata.Metadata(metadata, process_now=False) m = Metadata.Metadata(metadata, migrate=False)
self.assertEquals('0.0.1', m.version) self.assertEquals('0.0.1', m.version)
def test_assertion_if_there_is_an_extra_field(self): def test_validation_error_if_there_is_an_extra_field(self):
metadata = { metadata = {
'license': 'Oscilloscope Laboratories', 'license': 'Oscilloscope Laboratories',
'description': 'Four couples meet for Sunday brunch only to discover they are stuck in a house together as the world may be about to end.', 'description': 'Four couples meet for Sunday brunch only to discover they are stuck in a house together as the world may be about to end.',
@ -94,8 +94,8 @@ class MetadataTest(unittest.TestCase):
'thumbnail': 'http://ia.media-imdb.com/images/M/MV5BMTQwNjYzMTQ0Ml5BMl5BanBnXkFtZTcwNDUzODM5Nw@@._V1_SY1000_CR0,0,673,1000_AL_.jpg', 'thumbnail': 'http://ia.media-imdb.com/images/M/MV5BMTQwNjYzMTQ0Ml5BMl5BanBnXkFtZTcwNDUzODM5Nw@@._V1_SY1000_CR0,0,673,1000_AL_.jpg',
'MYSTERYFIELD': '?' 'MYSTERYFIELD': '?'
} }
with self.assertRaises(AssertionError): with self.assertRaises(ValidationError):
Metadata.Metadata(metadata, process_now=False) Metadata.Metadata(metadata, migrate=False)
def test_version_is_002_if_all_fields_are_present(self): def test_version_is_002_if_all_fields_are_present(self):
metadata = { metadata = {
@ -112,7 +112,7 @@ class MetadataTest(unittest.TestCase):
'content-type': 'video/mp4', 'content-type': 'video/mp4',
'thumbnail': 'https://svs.gsfc.nasa.gov/vis/a010000/a012000/a012034/Combined.00_08_16_17.Still004.jpg' 'thumbnail': 'https://svs.gsfc.nasa.gov/vis/a010000/a012000/a012034/Combined.00_08_16_17.Still004.jpg'
} }
m = Metadata.Metadata(metadata, process_now=False) m = Metadata.Metadata(metadata, migrate=False)
self.assertEquals('0.0.2', m.version) self.assertEquals('0.0.2', m.version)
def test_version_is_003_if_all_fields_are_present(self): def test_version_is_003_if_all_fields_are_present(self):
@ -130,7 +130,7 @@ class MetadataTest(unittest.TestCase):
'content_type': 'video/mp4', 'content_type': 'video/mp4',
'thumbnail': 'https://svs.gsfc.nasa.gov/vis/a010000/a012000/a012034/Combined.00_08_16_17.Still004.jpg' 'thumbnail': 'https://svs.gsfc.nasa.gov/vis/a010000/a012000/a012034/Combined.00_08_16_17.Still004.jpg'
} }
m = Metadata.Metadata(metadata, process_now=False) m = Metadata.Metadata(metadata, migrate=False)
self.assertEquals('0.0.3', m.version) self.assertEquals('0.0.3', m.version)
def test_version_claimed_is_001_but_version_is_002(self): def test_version_claimed_is_001_but_version_is_002(self):
@ -148,8 +148,8 @@ class MetadataTest(unittest.TestCase):
'content-type': 'video/mp4', 'content-type': 'video/mp4',
'thumbnail': 'https://svs.gsfc.nasa.gov/vis/a010000/a012000/a012034/Combined.00_08_16_17.Still004.jpg' 'thumbnail': 'https://svs.gsfc.nasa.gov/vis/a010000/a012000/a012034/Combined.00_08_16_17.Still004.jpg'
} }
with self.assertRaises(AssertionError): with self.assertRaises(ValidationError):
Metadata.Metadata(metadata, process_now=False) Metadata.Metadata(metadata, migrate=False)
def test_version_claimed_is_002_but_version_is_003(self): def test_version_claimed_is_002_but_version_is_003(self):
metadata = { metadata = {
@ -166,8 +166,8 @@ class MetadataTest(unittest.TestCase):
'content_type': 'video/mp4', 'content_type': 'video/mp4',
'thumbnail': 'https://svs.gsfc.nasa.gov/vis/a010000/a012000/a012034/Combined.00_08_16_17.Still004.jpg' 'thumbnail': 'https://svs.gsfc.nasa.gov/vis/a010000/a012000/a012034/Combined.00_08_16_17.Still004.jpg'
} }
with self.assertRaises(AssertionError): with self.assertRaises(ValidationError):
Metadata.Metadata(metadata, process_now=False) Metadata.Metadata(metadata, migrate=False)
def test_version_001_ports_to_003(self): def test_version_001_ports_to_003(self):
metadata = { metadata = {
@ -181,7 +181,7 @@ class MetadataTest(unittest.TestCase):
'content-type': 'audio/mpeg', 'content-type': 'audio/mpeg',
'thumbnail': 'http://ia.media-imdb.com/images/M/MV5BMTQwNjYzMTQ0Ml5BMl5BanBnXkFtZTcwNDUzODM5Nw@@._V1_SY1000_CR0,0,673,1000_AL_.jpg', 'thumbnail': 'http://ia.media-imdb.com/images/M/MV5BMTQwNjYzMTQ0Ml5BMl5BanBnXkFtZTcwNDUzODM5Nw@@._V1_SY1000_CR0,0,673,1000_AL_.jpg',
} }
m = Metadata.Metadata(metadata, process_now=True) m = Metadata.Metadata(metadata, migrate=True)
self.assertEquals('0.0.3', m.version) self.assertEquals('0.0.3', m.version)
def test_version_002_ports_to_003(self): def test_version_002_ports_to_003(self):
@ -199,5 +199,5 @@ class MetadataTest(unittest.TestCase):
'content-type': 'video/mp4', 'content-type': 'video/mp4',
'thumbnail': 'https://svs.gsfc.nasa.gov/vis/a010000/a012000/a012034/Combined.00_08_16_17.Still004.jpg' 'thumbnail': 'https://svs.gsfc.nasa.gov/vis/a010000/a012000/a012034/Combined.00_08_16_17.Still004.jpg'
} }
m = Metadata.Metadata(metadata, process_now=True) m = Metadata.Metadata(metadata, migrate=True)
self.assertEquals('0.0.3', m.version) self.assertEquals('0.0.3', m.version)