Repository: lamenezes/simple-model

Issue:
Allow model creation from list
In order to create models from a list currently we have to do the following:

```python
my_list = [{'foo': 1, 'bar': 2}, {'foo': 0, 'bar': -10}]
models = [MyModel(**data) for data in my_list]
```

It would be nice to have a SimpleModel classmethod that did it for us in order to avoid repetition and minor mistakes:

```python
my_list = [{'foo': 1, 'bar': 2}, {'foo': 0, 'bar': -10}]
models = MyModel.build_many(my_list)
```

Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/13518910/badge)](https://coveralls.io/builds/13518910)

Coverage remained the same at 100.0% when pulling **83f3b000306665f385e93e4fecb8daf2ae8c3696 on dirtycoder:add-build-many** into **b81430b959e88963cf2a46145491a30a1de03dd1 on lamenezes:master**.

lamenezes: Thanks for the PR @dirtycoder! :smile: 
lamenezes: Also, would you please document this new functionality to the README? Thanks again @dirtycoder!
coveralls: 
[![Coverage Status](https://coveralls.io/builds/13693764/badge)](https://coveralls.io/builds/13693764)

Coverage remained the same at 100.0% when pulling **783c9cd8cca5fd50a5a671fbff9cb23b3fd07b4c on dirtycoder:add-build-many** into **b81430b959e88963cf2a46145491a30a1de03dd1 on lamenezes:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/13693897/badge)](https://coveralls.io/builds/13693897)

Coverage remained the same at 100.0% when pulling **18afe6a2a2820c1e97996b755b77426a00ddb054 on dirtycoder:add-build-many** into **f811032749a8c0f5b3415d7b25e2b76fcc16b877 on lamenezes:master**.


Patch:
diff --git a/.gitignore b/.gitignore
index 0ad2f42..ce23a29 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,4 @@ coverage.xml
 *~
 [._]*.s[a-w][a-z]
 .mypy_cache
+.idea
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9947a37..3705fe3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,5 +1,5 @@
 -   repo: git@github.com:pre-commit/pre-commit-hooks
-    sha: 516cc9fa72ad09699f2c03ffbd0aa7f60d75b59a
+    sha: v0.9.5
     hooks:
     - id: debug-statements
     - id: trailing-whitespace
diff --git a/README.rst b/README.rst
index 1341afd..b0deefc 100644
--- a/README.rst
+++ b/README.rst
@@ -14,9 +14,9 @@ plenty of lists and dicts.
 
 It has simple objectives:
 
-- Define your fields easily (just a tuple, nor dicts or instances of type classes whatever)
+- Define your fields easily (just a tuple, not dicts or instances of type classes whatever)
 - Support for field validation
-- Conversion to dict
+- Convert to dict
 
 That's it. If you want something more complex there are plenty of libraries and
 frameworks that does a lot of cool stuff.
@@ -114,6 +114,18 @@ easily done using simple-model:
     ('John Doe', 10)
 
 
+Build many models
+-----------------
+
+It's possible to build many models in a single step, it can be done by passing an iterable
+to the `build_many` method.
+
+.. code:: python
+
+    people = [{'name': 'John Doe'}, {'name': 'John Doe II'}]
+    models = Person.build_many(people)
+
+
 Conversion to Dict
 ------------------
 
@@ -149,7 +161,6 @@ It also supports nested models as lists:
     >> person = Person(name='Jane Doe', age=60)
     >> other_person = Person(name='John Doe', age=15)
     >> social_person = MoreSocialPerson(name='Foo Bar', friends=[person, other_person])
-    >> dict(social_person)
     {
         'name': 'Foo Bar',
         'friends': [
diff --git a/simple_model/models.py b/simple_model/models.py
index 0b04d0b..2ba0244 100644
--- a/simple_model/models.py
+++ b/simple_model/models.py
@@ -30,6 +30,17 @@ class BaseModel(Iterable[Tuple[str, Any]]):
         )
         return '{class_name}({attrs})'.format(class_name=type(self).__name__, attrs=attrs)
 
+    @classmethod
+    def build_many(cls, source: Iterable) -> list:
+        if not source:
+            raise ValueError('source should have at least one item')
+
+        keys_sets = [set(d.keys()) for d in source]
+        if any(map(lambda x: x ^ keys_sets[0], keys_sets)):
+            raise ValueError('All elements in source should have the same keys')
+
+        return [cls(**item) for item in source]
+
     def _get_fields(self) -> Iterator[ModelField]:
         for field_name in self.get_fields():
             field_value = getattr(self, field_name)


Test patch:
diff --git a/tests/test_model.py b/tests/test_model.py
index 6b8d74b..99179c3 100644
--- a/tests/test_model.py
+++ b/tests/test_model.py
@@ -23,6 +23,15 @@ def nested_model():
     return child
 
 
+@pytest.fixture
+def many_source():
+    return (
+        {'foo': '1 foo', 'bar': '1 bar', 'qux': '1 qux'},
+        {'foo': '2 foo', 'bar': '2 bar', 'qux': '2 qux'},
+        {'foo': '3 foo', 'bar': '3 bar', 'qux': '3 qux'},
+    )
+
+
 def test_base_model(base_model):
     assert base_model.foo == 'foo'
     assert base_model.bar == 'bar'
@@ -337,3 +346,22 @@ def test_dynamic_model_get_fields():
     fields = model.get_fields()
     assert '_private' not in fields
     assert len(fields) == 3
+
+
+def test_build_many(many_source):
+    models = BaseModel.build_many(many_source)
+
+    assert len(models) == 3
+    assert models[0].foo == '1 foo'
+    assert models[1].bar == '2 bar'
+    assert models[2].qux == '3 qux'
+
+
+def test_build_many_empty_iterable():
+    with pytest.raises(ValueError):
+        BaseModel.build_many([])
+
+
+def test_build_many_different_items():
+    with pytest.raises(ValueError):
+        BaseModel.build_many([{'a': 1}, {'b': 2}])