diff --git a/.travis.yml b/.travis.yml index e33de19..af017cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,9 @@ install: - pip install -r requirements-dev.txt - pip install -e . - pip install coveralls + - pip install flake8 script: + - flake8 elasticgit - py.test elasticgit -s --cov ./elasticgit after_success: - coveralls diff --git a/elasticgit/__init__.py b/elasticgit/__init__.py index 16f63a3..e480eb9 100644 --- a/elasticgit/__init__.py +++ b/elasticgit/__init__.py @@ -1,19 +1,6 @@ import pkg_resources -import sys from elasticgit.workspace import EG, F, Q __all__ = ['EG', 'F', 'Q'] __version__ = pkg_resources.require('elastic-git')[0].version - -version_info = { - 'language': 'python', - 'language_version_string': sys.version, - 'language_version': '%d.%d.%d' % ( - sys.version_info.major, - sys.version_info.minor, - sys.version_info.micro, - ), - 'package': 'elastic-git', - 'package_version': __version__ -} diff --git a/elasticgit/commands/avro.py b/elasticgit/commands/avro.py index 886be5b..bc0d7e2 100644 --- a/elasticgit/commands/avro.py +++ b/elasticgit/commands/avro.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + from jinja2 import Environment, PackageLoader from functools import partial import argparse @@ -7,21 +9,23 @@ from datetime import datetime -from elasticgit import version_info +import avro.schema + from elasticgit.models import ( - Model, IntegerField, TextField, ModelVersionField, FloatField, - BooleanField, ListField, DictField, UUIDField) + Model, IntegerField, TextField, FloatField, + BooleanField, ListField, DictField, UUIDField, + version_info) from elasticgit.commands.base import ( ToolCommand, ToolCommandError, CommandArgument) from elasticgit.utils import load_class -def deserialize(schema, field_mapping={}, module_name=None): +def deserialize(data, field_mapping={}, module_name=None): """ Deserialize an Avro schema and define it within a module (if specified) - :param dict schema: + :param dict data: The Avro schema :param dict field_mapping: Optional mapping to override the default mapping. @@ -47,6 +51,7 @@ def deserialize(schema, field_mapping={}, module_name=None): """ schema_loader = SchemaLoader() + schema = avro.schema.make_avsc_object(data, avro.schema.Names()).to_json() model_code = schema_loader.generate_model(schema) model_name = schema['name'] @@ -165,13 +170,11 @@ class SchemaLoader(ToolCommand): action='append', type=RenameType), ) - mapping = { + core_mapping = { 'int': IntegerField, 'string': TextField, 'float': FloatField, 'boolean': BooleanField, - 'array': ListField, - 'record': DictField, } def run(self, schema_files, field_mappings=None, model_renames=None): @@ -212,15 +215,20 @@ def field_class_for(self, field, field_mapping): if isinstance(field_type, dict): return self.field_class_for_complex_type(field) - return self.mapping[field_type].__name__ + return self.core_mapping[field_type].__name__ def field_class_for_complex_type(self, field): field_type = field['type'] - if (field_type['name'] == 'ModelVersionField' and - field_type['namespace'] == 'elasticgit.models'): - return ModelVersionField.__name__ + handler = getattr( + self, 'field_class_for_complex_%(type)s_type' % field_type) + return handler(field) + + def field_class_for_complex_record_type(self, field): return DictField.__name__ + def field_class_for_complex_array_type(self, field): + return ListField.__name__ + def default_value(self, field): return pprint.pformat(field['default'], indent=8) @@ -274,6 +282,9 @@ def generate_model(self, schema, field_mapping={}, model_renames={}, env.globals['field_class_for'] = partial( self.field_class_for, field_mapping=field_mapping) env.globals['default_value'] = self.default_value + env.globals['is_complex'] = ( + lambda field: isinstance(field['type'], dict)) + env.globals['core_mapping'] = self.core_mapping template = env.get_template('model_generator.py.txt') return template.render( @@ -301,41 +312,13 @@ class SchemaDumper(ToolCommand): CommandArgument('class_path', help='python path to Class.'), ) - mapping = { + # How model fields map to types + core_field_mappings = { IntegerField: 'int', TextField: 'string', FloatField: 'float', BooleanField: 'boolean', - ListField: 'array', - DictField: 'record', UUIDField: 'string', - ModelVersionField: { - 'type': 'record', - 'name': 'ModelVersionField', - 'namespace': 'elasticgit.models', - 'fields': [ - { - 'name': 'language', - 'type': 'string', - }, - { - 'name': 'language_version_string', - 'type': 'string', - }, - { - 'name': 'language_version', - 'type': 'string', - }, - { - 'name': 'package', - 'type': 'string', - }, - { - 'name': 'package_version', - 'type': 'string', - } - ] - } } def run(self, class_path): @@ -370,6 +353,32 @@ def dump_schema(self, model_class): for name, field in model_class._fields.items()], }, indent=2) + def map_field_to_type(self, field): + if field.__class__ in self.core_field_mappings: + return self.core_field_mappings[field.__class__] + + handler = getattr(self, 'map_%s_type' % (field.__class__.__name__,)) + return handler(field) + + def map_ListField_type(self, field): + return { + 'type': 'array', + 'name': field.name, + 'namespace': field.__class__.__module__, + 'items': [self.map_field_to_type(fld) for fld in field.fields], + } + + def map_DictField_type(self, field): + return { + 'type': 'record', + 'name': field.name, + 'namespace': field.__class__.__module__, + 'fields': [{ + 'name': fld.name, + 'type': self.map_field_to_type(fld), + } for fld in field.fields], + } + def get_field_info(self, name, field): """ Return the Avro field object for an @@ -383,7 +392,7 @@ def get_field_info(self, name, field): """ return { 'name': name, - 'type': self.mapping[field.__class__], + 'type': self.map_field_to_type(field), 'doc': field.doc, 'default': field.default, 'aliases': [fallback.field_name for fallback in field.fallbacks] diff --git a/elasticgit/commands/gitmodel.py b/elasticgit/commands/gitmodel.py index 3b8a6b9..1ff830b 100644 --- a/elasticgit/commands/gitmodel.py +++ b/elasticgit/commands/gitmodel.py @@ -153,6 +153,9 @@ def guess_type(self, value): float: 'float', str: 'string', unicode: 'string', - list: 'array', + list: { + 'type': 'array', + 'items': ['string'], + }, None: 'null', }[None if value is None else type(value)] diff --git a/elasticgit/commands/tests/test_avro.py b/elasticgit/commands/tests/test_avro.py index 604ebf3..ff24ff3 100644 --- a/elasticgit/commands/tests/test_avro.py +++ b/elasticgit/commands/tests/test_avro.py @@ -4,8 +4,6 @@ from elasticgit import models from elasticgit.tests.base import ToolBaseTest -import elasticgit - class TestDumpSchemaTool(ToolBaseTest): @@ -47,6 +45,18 @@ class TestModel(models.Model): age = self.get_field(schema, 'age') self.assertEqual(age['aliases'], ['length']) + def test_dump_array(self): + class TestModel(models.Model): + tags = models.ListField('The tags', + fields=(models.IntegerField('doc'),)) + + schema_dumper = self.mk_schema_dumper() + schema = json.loads(schema_dumper.dump_schema(TestModel)) + tags = self.get_field(schema, 'tags') + field_type = tags['type'] + self.assertEqual(field_type['type'], 'array') + self.assertEqual(field_type['items'], ['int']) + class TestLoadSchemaTool(ToolBaseTest): @@ -124,7 +134,10 @@ def test_boolean_field(self): def test_array_field(self): self.assertFieldCreation({ 'name': 'array', - 'type': 'array', + 'type': { + 'type': 'array', + 'items': ['string'], + }, 'doc': 'The Array', 'default': ['foo', 'bar', 'baz'] }, models.ListField) @@ -132,7 +145,14 @@ def test_array_field(self): def test_dict_field(self): self.assertFieldCreation({ 'name': 'obj', - 'type': 'record', + 'type': { + 'type': 'record', + 'items': ['string'], + 'fields': [{ + 'name': 'hello', + 'type': 'string', + }] + }, 'doc': 'The Object', 'default': {'hello': 'world'}, }, models.DictField) @@ -143,7 +163,12 @@ def test_complex_field(self): 'type': { 'namespace': 'foo.bar', 'name': 'ItIsComplicated', - 'type': 'record' + 'type': 'record', + 'items': ['string'], + 'fields': [{ + 'name': 'foo', + 'type': 'string', + }] }, 'doc': 'Super Complex', 'default': {}, @@ -154,12 +179,13 @@ def test_version_field(self): 'name': 'version', 'type': { 'namespace': 'elasticgit.models', - 'name': 'ModelVersionField', + 'name': 'version', 'type': 'record', + 'items': ['string'], }, 'doc': 'The Model Version', - 'default': elasticgit.version_info, - }, models.ModelVersionField) + 'default': models.version_info, + }, models.DictField) def test_mapping_hints(self): self.assertFieldCreation({ @@ -177,8 +203,12 @@ class DumpAndLoadModel(models.Model): integer = models.IntegerField('the integer') float_ = models.FloatField('the float') boolean = models.BooleanField('the boolean') - list_ = models.ListField('the list') - dict_ = models.DictField('the dict') + list_ = models.ListField('the list', fields=( + models.IntegerField('the int'), + )) + dict_ = models.DictField('the dict', fields=( + models.TextField('hello', name='hello'), + )) class TestDumpAndLoad(ToolBaseTest): @@ -189,6 +219,7 @@ def test_two_way(self): schema_loader = self.mk_schema_loader() schema = schema_dumper.dump_schema(DumpAndLoadModel) + generated_code = schema_loader.generate_model(json.loads(schema)) GeneratedModel = self.load_class(generated_code, 'DumpAndLoadModel') @@ -198,7 +229,7 @@ def test_two_way(self): 'integer': 1, 'float': 1.1, 'boolean': False, - 'list': ['1', '2', '3'], + 'list_': [1, 2, 3], 'dict_': {'hello': 'world'} } record1 = DumpAndLoadModel(data) @@ -220,8 +251,8 @@ def test_two_way_dict_ints(self): 'integer': 1, 'float': 1.1, 'boolean': False, - 'list': ['1', '2', '3'], - 'dict_': {'hello': 1} + 'list_': [1, 2, 3], + 'dict_': {'hello': '1'} } record1 = DumpAndLoadModel(data) record2 = GeneratedModel(data) @@ -242,7 +273,7 @@ def test_two_way_list_ints(self): 'integer': 1, 'float': 1.1, 'boolean': False, - 'list': [1, 2, 3], + 'list_': [1, 2, 3], 'dict_': {'hello': '1'} } record1 = DumpAndLoadModel(data) @@ -264,7 +295,7 @@ def test_two_way_list_unicode(self): 'integer': 1, 'float': 1.1, 'boolean': False, - 'list': [1, 2, 3], + 'list_': [1, 2, 3], 'dict_': {'hello': '1'} } record1 = DumpAndLoadModel(data) @@ -326,7 +357,7 @@ def test_load_older_version(self): class Foo(models.Model): pass - old_version_info = elasticgit.version_info.copy() + old_version_info = models.version_info.copy() old_version_info['package_version'] = '0.0.1' f = Foo({ @@ -341,9 +372,9 @@ class Foo(models.Model): pass major, minor, micro = map( - int, elasticgit.version_info['package_version'].split('.')) + int, models.version_info['package_version'].split('.')) - new_version = elasticgit.version_info.copy() + new_version = models.version_info.copy() new_version['package_version'] = '%d.%d.%d' % ( major + 1, minor, diff --git a/elasticgit/commands/tests/test_gitmodel.py b/elasticgit/commands/tests/test_gitmodel.py index 522c070..883f1f3 100644 --- a/elasticgit/commands/tests/test_gitmodel.py +++ b/elasticgit/commands/tests/test_gitmodel.py @@ -146,7 +146,10 @@ def test_introspect_page_schema(self): ('created_at', 'string'), ('featured_in_category', 'boolean'), ('modified_at', 'string'), - ('linked_pages', 'array'), + ('linked_pages', { + 'type': 'array', + 'items': ['string'], + }), ('slug', 'string'), ('content', 'string'), ('source', 'string'), # inferred null but default type is string diff --git a/elasticgit/commands/tests/test_version.py b/elasticgit/commands/tests/test_version.py index e6dbe83..0adc36e 100644 --- a/elasticgit/commands/tests/test_version.py +++ b/elasticgit/commands/tests/test_version.py @@ -1,7 +1,7 @@ from StringIO import StringIO import json -from elasticgit import version_info +from elasticgit.models import version_info from elasticgit.tests.base import ToolBaseTest from elasticgit.commands.version import VersionTool, DEFAULT_FILE_NAME diff --git a/elasticgit/commands/version.py b/elasticgit/commands/version.py index 9ae5ead..c0e1c6f 100644 --- a/elasticgit/commands/version.py +++ b/elasticgit/commands/version.py @@ -1,7 +1,7 @@ import sys import json -from elasticgit import version_info +from elasticgit.models import version_info from elasticgit.commands.base import ToolCommand, CommandArgument diff --git a/elasticgit/models.py b/elasticgit/models.py index 92c05bd..a712bf3 100644 --- a/elasticgit/models.py +++ b/elasticgit/models.py @@ -1,3 +1,5 @@ +import sys +import pkg_resources from copy import deepcopy from urllib2 import urlparse import uuid @@ -7,7 +9,17 @@ from confmodel.fallbacks import SingleFieldFallback -import elasticgit +version_info = { + 'language': 'python', + 'language_version_string': sys.version, + 'language_version': '%d.%d.%d' % ( + sys.version_info.major, + sys.version_info.minor, + sys.version_info.micro, + ), + 'package': 'elastic-git', + 'package_version': pkg_resources.require('elastic-git')[0].version +} class ModelField(ConfigField): @@ -17,13 +29,18 @@ class ModelField(ConfigField): } def __init__(self, doc, required=False, default=None, static=False, - fallbacks=(), mapping={}): + fallbacks=(), mapping={}, name=None): super(ModelField, self).__init__( doc, required=required, default=default, static=static, fallbacks=fallbacks) + self.name = name self.mapping = self.__class__.default_mapping.copy() self.mapping.update(mapping) + def __repr__(self): + return '<%s.%s %r>' % ( + self.__class__.__module__, self.__class__.__name__, self.name) + class TextField(ModelField): """ @@ -115,11 +132,24 @@ class ListField(ModelField): 'type': 'string', } + def __init__(self, doc, fields, default=[], static=False, + fallbacks=(), mapping={}): + super(ListField, self).__init__( + doc, default=default, static=static, fallbacks=fallbacks, + mapping=mapping) + self.fields = fields + def clean(self, value): if isinstance(value, tuple): value = list(value) if not isinstance(value, list): self.raise_config_error("is not a list.") + + if len(value) > 0: + for field in self.fields: + if not any([field.clean(v) for v in value]): + self.raise_config_error( + 'All field checks failed for some values.') return deepcopy(value) @@ -129,16 +159,34 @@ class DictField(ModelField): """ field_type = 'dict' - #: Mapping for Elasticsearch - default_mapping = { - 'type': 'string', - } + def __init__(self, doc, fields, default=None, static=False, + fallbacks=(), mapping=()): + mapping = mapping or self.generate_default_mapping(fields) + super(DictField, self).__init__( + doc, default=default, static=static, fallbacks=fallbacks, + mapping=mapping) + self.fields = fields + + def generate_default_mapping(self, fields): + field_names = [field.name for field in fields] + return { + 'type': 'nested', + 'properties': dict( + [(name, {'type': 'string'}) for name in field_names]), + } def clean(self, value): if not isinstance(value, dict): - self.raise_config_error("is not a dict.") + self.raise_config_error('is not a dict.') return deepcopy(value) + def validate(self, config): + data = self.get_value(config) + if data: + for key, value in data.items(): + [field] = [field for field in self.fields if field.name == key] + field.clean(value) + class URLField(ModelField): """ @@ -160,39 +208,6 @@ def clean(self, value): return urlparse.urlparse(value) -class ModelVersionField(DictField): - """ - A field holding the version information for a model - """ - default_mapping = { - 'type': 'nested', - 'properties': { - 'language': {'type': 'string'}, - 'language_version_string': {'type': 'string'}, - 'language_version': {'type': 'string'}, - 'package': {'type': 'string'}, - 'package_version': {'type': 'string'} - } - } - - def compatible_version(self, own_version, check_version): - own = map(int, own_version.split('.')) - check = map(int, check_version.split('.')) - return own >= check - - def validate(self, config): - config._config_data.setdefault( - self.name, elasticgit.version_info.copy()) - value = self.get_value(config) - current_version = elasticgit.version_info['package_version'] - package_version = value['package_version'] - if not self.compatible_version(current_version, package_version): - raise ConfigError( - 'Got a version from the future, expecting: %r got %r' % ( - current_version, package_version)) - return super(ModelVersionField, self).validate(config) - - class UUIDField(TextField): def validate(self, config): @@ -211,7 +226,28 @@ class Model(Config): A dictionary with keys & values to populate this Model instance with. """ - _version = ModelVersionField('Model Version Identifier') + _version = DictField( + 'Model Version Identifier', + default=version_info, + fields=( + TextField('language', name='language'), + TextField('language_version_string', + name='language_version_string'), + TextField('language_version', name='language_version'), + TextField('package', name='package'), + TextField('package_version', name='package_version'), + ), + mapping={ + 'type': 'nested', + 'properties': { + 'language': {'type': 'string'}, + 'language_version_string': {'type': 'string'}, + 'language_version': {'type': 'string'}, + 'package': {'type': 'string'}, + 'package_version': {'type': 'string'} + } + }) + uuid = UUIDField('Unique Identifier') def __init__(self, config_data, static=False): @@ -245,5 +281,21 @@ def __iter__(self): for field in self._get_fields(): yield field.name, field.get_value(self) + def compatible_version(self, own_version, check_version): + own = map(int, own_version.split('.')) + check = map(int, check_version.split('.')) + return own >= check + + def post_validate(self): + value = self._version + current_version = version_info['package_version'] + package_version = value['package_version'] + if not self.compatible_version(current_version, package_version): + raise ConfigError( + 'Got a version from the future, expecting: %r got %r' % ( + current_version, package_version)) + super(Model, self).post_validate() + + ConfigError SingleFieldFallback diff --git a/elasticgit/templates/model_generator.py.txt b/elasticgit/templates/model_generator.py.txt index ad7f4ec..3ff78f9 100644 --- a/elasticgit/templates/model_generator.py.txt +++ b/elasticgit/templates/model_generator.py.txt @@ -22,6 +22,16 @@ class {{model_class_for(schema.name)}}(models.Model): {{field.name}} = models.{{ field_class_for(field) }}( {%- if field.doc -%}u"""{{ field.docĀ }}"""{% else %}"""{{ field.name }}"""{% endif %} {%- if field.default is defined -%}, default={{ default_value(field) }}{% endif %} + {%- if is_complex(field) -%}, fields=( + {%- set sub_type = field['type'] %} + {%- if sub_type['type'] == 'array' %} + {% for item in sub_type['items'] %} + models.{{core_mapping[item].__name__}}('{{item}}'),{%- endfor %} + {%- elif sub_type['type'] == 'record' %} + {% for field in sub_type['fields'] %} + models.{{core_mapping[field.type].__name__}}('{{field.name}}', name='{{field.name}}'),{%- endfor %} + {% endif %} + ){% endif %} {%- if field.aliases -%}, fallbacks=[{% for alias in field.aliases -%} models.SingleFieldFallback('{{alias}}'), {%- endfor %}]{% endif %}){% endfor %} diff --git a/elasticgit/tests/base.py b/elasticgit/tests/base.py index af1073e..fdf1b50 100644 --- a/elasticgit/tests/base.py +++ b/elasticgit/tests/base.py @@ -7,6 +7,8 @@ from unittest import TestCase +import avro.schema + from elasticgit.models import ( IntegerField, TextField, Model, SingleFieldFallback) from elasticgit.workspace import EG @@ -87,7 +89,9 @@ def mk_schema_dumper(self): return schema_dumper def get_schema(self, schema_dumper): - return json.loads(schema_dumper.stdout.getvalue()) + data = schema_dumper.stdout.getvalue() + schema = avro.schema.parse(data) + return schema.to_json() def get_field(self, schema, field_name): return [field diff --git a/elasticgit/tests/test_index.py b/elasticgit/tests/test_index.py index f853a78..92e7e22 100644 --- a/elasticgit/tests/test_index.py +++ b/elasticgit/tests/test_index.py @@ -1,9 +1,8 @@ +from elasticgit.models import version_info from elasticgit.tests.base import ModelBaseTest, TestPerson from elasticutils import S -import elasticgit - class TestIndex(ModelBaseTest): @@ -38,7 +37,7 @@ def test_extract_document_with_object(self): 'age': 1, 'name': 'Kees', 'uuid': person.uuid, - '_version': elasticgit.version_info, + '_version': version_info, }) def test_extract_document_with_object_id(self): @@ -53,7 +52,7 @@ def test_extract_document_with_object_id(self): 'age': 1, 'name': 'Kees', 'uuid': person.uuid, - '_version': elasticgit.version_info, + '_version': version_info, }) def test_indexing(self): diff --git a/elasticgit/tests/test_manager.py b/elasticgit/tests/test_manager.py index f82e31e..2191bca 100644 --- a/elasticgit/tests/test_manager.py +++ b/elasticgit/tests/test_manager.py @@ -3,7 +3,7 @@ import os from elasticgit import EG -from elasticgit.models import IntegerField, ModelVersionField +from elasticgit.models import IntegerField from elasticgit.tests.base import ModelBaseTest, TestPage, TestPerson from elasticsearch.client import Elasticsearch @@ -50,7 +50,7 @@ def test_indexable(self): 'properties': { 'age': {'type': 'integer'}, 'uuid': {'type': 'string'}, - '_version': ModelVersionField.default_mapping, + '_version': model_class._fields['_version'].mapping, } }) model_instance = model_class({'age': 1}) diff --git a/elasticgit/tests/test_models.py b/elasticgit/tests/test_models.py index 7834a90..1aa69ce 100644 --- a/elasticgit/tests/test_models.py +++ b/elasticgit/tests/test_models.py @@ -1,8 +1,7 @@ from elasticgit.tests.base import ModelBaseTest from elasticgit.models import ( - ConfigError, IntegerField, TextField, ModelVersionField) - -import elasticgit + ConfigError, IntegerField, TextField, ListField, version_info, + DictField) class TestModel(ModelBaseTest): @@ -42,7 +41,7 @@ def test_to_dict(self): 'age': IntegerField('An age'), 'name': TextField('A name'), }) - data = {'age': 1, 'name': 'foo', '_version': elasticgit.version_info} + data = {'age': 1, 'name': 'foo', '_version': version_info} model = model_class(data) self.assertEqual(dict(model), data) @@ -82,7 +81,37 @@ def test_update(self): self.assertFalse(new_model.is_read_only()) def test_version_check(self): - field = ModelVersionField('ModelVersionField') - self.assertTrue(field.compatible_version('0.2.10', '0.2.9')) - self.assertTrue(field.compatible_version('0.2.10', '0.2.10')) - self.assertFalse(field.compatible_version('0.2.9', '0.2.10')) + model_class = self.mk_model({}) + model = model_class({}) + self.assertTrue(model.compatible_version('0.2.10', '0.2.9')) + self.assertTrue(model.compatible_version('0.2.10', '0.2.10')) + self.assertFalse(model.compatible_version('0.2.9', '0.2.10')) + + def test_list_field(self): + model_class = self.mk_model({ + 'tags': ListField('list field', fields=( + IntegerField('int'), + )) + }) + self.assertRaises(ConfigError, model_class, {'tags': ['a']}) + self.assertRaises(ConfigError, model_class, {'tags': [2.0]}) + self.assertRaises(ConfigError, model_class, {'tags': [None]}) + self.assertTrue(model_class({'tags': []})) + self.assertTrue(model_class({'tags': [1, 2, 3]})) + self.assertTrue(model_class({'tags': ['1']})) + + def test_dict_field(self): + model_class = self.mk_model({ + 'foo': DictField('dict field', fields=( + TextField('a', name='a'), + TextField('b', name='b'), + )) + }) + self.assertEqual( + model_class._fields['foo'].mapping, { + 'type': 'nested', + 'properties': { + 'a': {'type': 'string'}, + 'b': {'type': 'string'}, + } + }) diff --git a/pytest.ini b/pytest.ini index 68d5566..1fe28e2 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,2 @@ [pytest] -addopts = --doctest-modules --verbose +addopts = --doctest-modules --verbose -s diff --git a/requirements.txt b/requirements.txt index 0302d52..ca1cfe0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ elasticutils==0.10.1 GitPython==0.3.5 Jinja2==2.7.3 Unidecode==0.04.16 +avro==1.7.7