bpmappers

コンテンツ:

bpmappers

build-status pypi docs

bpmappersは、Pythonの辞書の値やオブジェクトのプロパティを別の辞書へマッピングするPythonモジュールです。

インストール

pip を使ってインストールします。

$ pip install bpmappers

使い方

Personクラスのインスタンスを辞書に変換する例です:

>>> class Person:
...     def __init__(self, name, age):
...         self.name = name
...         self.age = age
...     def __repr__(self):
...         return "<Person name={}, age={}>".format(self.name, self.age)
...
>>> p = Person("Spam", 25)
>>> p
<Person name=Spam, age=25>
>>> from bpmappers import Mapper, RawField
>>> class PersonMapper(Mapper):
...     mapped_name = RawField('name')
...     mapped_age = RawField('age')
...
>>> PersonMapper(p).as_dict()
OrderedDict([('mapped_name', 'Spam'), ('mapped_age', 25)])

動作要件

  • Pythonのバージョン 2.7, 3.4, 3.5, 3.6, 3.7
  • Django>=1.11 (Djangoサポートを使用する場合)

ライセンス

MITライセンス

ドキュメント

最新のドキュメントはReadTheDocsでホストされています。

https://bpmappers.readthedocs.io/en/latest/

開発

このプロジェクトはGitHubでホストされています: https://github.com/beproud/bpmappers

作者

  • BeProud, Inc

メンテナ

開発の背景

WebAPIのレスポンスをJSONやXMLなどの構造化テキストで返すとき、アプリケーションのデータ構造とAPIのインターフェースの構造は一致しないことが頻繁にありました。

例えば、次のような構造のJSONを返すWebAPIの仕様があったとします(フォーラムのコメントデータ):

{
  "comment_id": 1,
  "content": "This is a comment.",
  "posted_by": {
    "user_id": 2,
    "name": "Sato",
    "age": 40
  }
}

このデータは、アプリケーション上では次のようなデータ構造で保持されていることがあります:

class User:
    def __init__(self, id: int, name: str, age: int):
        self.id = id
        self.name = name
        self.age = age

class Comment:
    def __init__(self, id: int, content: str, posted_by: User):
        self.id = id
        self.content = content
        self.posted_by = posted_by

user = User(1, "Sato", 40)
comment = Comment(1, "This is a comment.", user)

WebAPIの仕様に従ったJSONと等価の辞書を生成する場合、次のようなコードを書くかもしれません:

data = {
  'comment_id': comment.id,
  'content': comment.content,
  'posted_by': {
    'user_id': comment.posted_by.id,
    'name': comment.posted_by.name,
    'age': comment.posted_by.age
  }
}

posted_byの内容を返す部分を再利用したくなった場合、関数に分けるかもしれません:

def map_user(user: User):
    return {
        'user_id': user.id,
        'name': user.name,
        'age': user.age
    }

data = {
  'comment_id': comment.id,
  'content': comment.content,
  'posted_by': map_user(comment.posted_by)
}

このように再利用の単位で関数を作っていくと、似たような機能の関数がいくつも作られてしまうことがありました。また、リストをマッピングする場合はforループも使うことになり、マッピングのコードは見通しが悪くなってきます。

こうした背景から、bpmappersを開発しました。

bpmappersではマッピングルールを宣言的に書くことでき、コードはスッキリします。

bpmappersを使うとこのように書けます:

from bpmappers import Mapper, RawField, DelegateField
class UserMapper(Mapper):
    user_id = RawField('id')
    name = RawField()
    age = RawField()

class CommentMapper(Mapper):
    comment_id = RawField('id')
    content = RawField()
    posted_by = DelegateField(UserMapper)

data = CommentMapper(comment).as_dict()

他にもマッピング処理のフックポイントや、DjangoフレームワークのORM向けのサポートを含んでいます。

bpmappersを利用することで、あなたのデータマッピングのコードがスッキリすることを願っています。

基本的な使い方

マッピングクラスを定義する

bpmappersを使ってマッピングするためには、まずマッピングクラスを定義します。

マッピングクラスの例を示します:

from bpmappers import Mapper, RawField
class SpamMapper(Mapper):
    "マッピングクラス"
    dest_key = RawField('src_key')

マッピングクラスは、 bpmappers.Mapper を継承して定義します。 dest_key は、マッピング結果の辞書のキー名です。 src_key は、マッピングソースのオブジェクトの属性名、もしくは辞書のキー名です。 マッピングソースから指定した属性名(キー)で取得した値をそのままマッピング結果の値とする場合は RawField フィールドクラスを使います。

マッピングクラスを使う

定義したマッピングクラスは、次のように使います:

src_data = {'src_key': 'spam'}  # マッピングソース
result = SpamMapper(src_data).as_dict()
# resultは OrderedDict([('dest_key', 'spam')]) となる

マッピングクラスのコンストラクタに、マッピングソースとしてsrc_data変数に代入された辞書を渡しています。 as_dict() メソッドを呼ぶとマッピング処理が実行され、マッピング結果の辞書(OrderedDict)が返されます。

Note

バージョン0.9で変更: as_dict()メソッドで返される値は、SortedDictからOrderedDictに変更されました。

入れ子構造のオブジェクトをマッピングする(別のマッピングクラスに委譲する)

親オブジェクトがプロパティで子オブジェクトを持つような、入れ子構造のオブジェクトをマッピングする場合、マッピングクラスのフィールドクラスに bpmappers.DelegateField を使います。

>>> from bpmappers import  Mapper, RawField, DelegateField
>>> class Person(object):
...     def __init__(self, name):
...         self.name = name
...
>>> class Book(object):
...     def __init__(self, name, author):
...         self.name = name
...         self.author = author
...
>>> class PersonMapper(Mapper):
...     name = RawField()
...
>>> class BookMapper(Mapper):
...     name = RawField()
...     author = DelegateField(PersonMapper)
...
>>> p = Person('wozozo')
>>> b = Book('python book', p)
>>> mapper = BookMapper(b)
>>> print(mapper.as_dict())
OrderedDict([('name', 'python book'), ('author', OrderedDict([('name', 'wozozo')]))])

bpmappers.DelegateField には、引数としてMapperを継承したクラスを指定します。 この例では、マッピングソースの Book.author は、 PersonMapper でマッピングされるように定義しています。

入れ子構造のリストをマッピングする

親子関係のオブジェクトで子がリストになっている場合、 bpmappers.ListDelegateField を使います。

>>> from bpmappers import  Mapper, RawField, ListDelegateField
>>> class Person(object):
...     def __init__(self, name):
...         self.name = name
...
>>> class Team(object):
...     def __init__(self, name, members):
...         self.name = name
...         self.members = members
...
>>> class TeamMapper(Mapper):
...     name = RawField()
...     members = ListDelegateField(PersonMapper)
...
>>> p1 = Person('wozozo')
>>> p2 = Person('moriyoshi')
>>> t = Team('php', [p1, p2])
>>> mapper = TeamMapper(t)
>>> print(mapper.as_dict())
OrderedDict([('name', 'php'), ('members', [OrderedDict([('name', 'wozozo')]), OrderedDict([('name', 'moriyoshi')])])])

bpmappers.ListDelegateField には、引数としてMapperを継承したクラスを指定します。 この例では、 TeamMapper.members の値はリストとして展開されて、 PersonMapper を使ってマッピングを行うように定義されています。

DjangoのManyToManyFieldをマッピングする場合、ListDelegateFieldにはDjangoのManagerオブジェクトが渡されるため、filterパラメータを指定する必要があります。

>>> from django.db import models
>>> from bpmappers import Mapper, RawField, ListDelegateField
>>> class Person(models.Model):
...     name = models.CharField(max_length=10)
...
>>> class Group(models.Model):
...     name = models.CharField(max_length=10)
...     persons = models.ManyToManyField(Person)
...
>>> class PersonMapper(Mapper):
...     name = RawField()
...
>>> class GroupMapper(Mapper):
...     name = RawField()
...     # filterを指定する
...     persons = ListDelegateField(PersonMapper, filter=lambda manager: manager.all())
...
>>> person1 = Person.objects.create('wozozo', 123)
>>> person2 = Person.objects.create('feiz', 456)
>>> group = Group.objects.create('test')
>>> group.persons.add(person1)
>>> group.persons.add(person2)
>>> mapper = GroupMapper(group)
>>> print(mapper.as_dict())
{'name': 'test', [{'name': 'wozozo', 'val': 123}, {'name': 'feiz', 'val': 456}]}

ドット区切りのフィールド指定による参照

ドット区切りの指定で、深い階層の値を簡単に参照できます。

>>> from bpmappers import Mapper, RawField
>>> class HogeMapper(Mapper):
...     hoge = RawField('hoge.piyo.fuga')
...
>>> HogeMapper({'hoge': {'piyo': {'fuga': 123}}}).as_dict()
OrderedDict([('hoge', 123)])

複数の入力値を1つの値にまとめる

Mapper.data はインスタンス作成時に引数で与えたものが格納されています。 この例では、入力値としてリストを渡しています。

>>> from bpmappers import Mapper, NonKeyField
>>> class Person(object):
...     def __init__(self, name):
...         self.name = name
...
>>> class MultiDataSourceMapper(Mapper):
...     pair = NonKeyField()
...     def filter_pair(self):
...         return '%s-%s' % (self.data[0].name, self.data[1].name)
...
>>> MultiDataSourceMapper([Person('foo'), Person('bar')]).as_dict()
OrderedDict([('pair', 'foo-bar')])

フックポイント

マッピング処理の途中で何か追加の処理を行いたい場合、いくつかのフックポイントを使用できます。

Mapper.filter_FOO

フィールドの値変換の前に実行されます。FOOはフィールド名に置き換えてください。

NonKeyField を使った場合、ここでマッピングに利用する値を生成することができます。

>>> from bpmappers import Mapper, NonKeyField
>>> class MyMapper(Mapper):
...     value = NonKeyField()
...     def filter_value(self):
...         return 10
...
>>> mapper = MyMapper()
>>> mapper.as_dict()
OrderedDict([('value', 10)])

Mapper.after_filter_FOO

フィールドの値変換の後に実行されます。FOOはフィールド名に置き換えてください。 第一引数に、filter_FOOの結果の値が入ります。

>>> from bpmappers import Mapper, NonKeyField
>>> class MyMapper(Mapper):
...     value = NonKeyField()
...     def filter_value(self):
...         return "oyoyo"
...
...     def after_filter_value(self, val):
...         return val.capitalize()
...
>>> mapper = MyMapper()
>>> print(mapper.as_dict())
OrderedDict([('value', 'Oyoyo')])

Mapper.attach_FOO

マッピングの結果の辞書に値を追加する代わりに実行されます。値を追加しない場合や、値の追加位置を変更する場合などに使用できます。

>>> from bpmappers import Mapper, NonKeyField, RawField
>>> class Point(object):
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...
>>> class PointMapper(Mapper):
...     x = RawField("x")
...     y = RawField("y")
...
...     def attach_x(self, parsed, v):
...         parsed[v] = (v, v*v, v*v*v, v*v*v*v)
...
...     def attach_y(self, parsed, v):
...         parsed[v] = "y is %s" % v
...
>>> mapper = PointMapper(Point(10, 20))
>>> print(mapper.as_dict())
OrderedDict([(10, (10, 100, 1000, 10000)), (20, 'y is 20')])

Field.callback

フィールドの値変換の前に実行されます。 filter_FOO の後にフィールドクラスで実行されます。

>>> from bpmappers import Mapper, RawField, DelegateField
>>> class Person(object):
...     def __init__(self, name):
...        self.name = name
...
>>> class PersonInfoMapper(Mapper):
...     info = RawField("name", callback = lambda v : "name:%s" % v)
...
>>>
>>> class PersonInfoMapper2(Mapper):
...     info = RawField("name", callback = lambda v : "name:%s" % v)
...
...     def filter_info(self, v):
...         return v+v
...
>>> mapper = PersonInfoMapper(Person("bucho"))
>>> print(mapper.as_dict())
OrderedDict([('info', 'name:bucho')])
>>> mapper = PersonInfoMapper2(Person("bucho"))
>>> print(mapper.as_dict())
OrderedDict([('info', 'name:buchobucho')])

Field.after_callback

フィールドの値変換の後に実行されます。 after_filter_FOO の前にフィールドクラスで実行されます。

>>> from bpmappers import Mapper, RawField, ListDelegateField
>>> class Person(object):
...     def __init__(self, name):
...         self.name = name
...
>>> class Book(object):
...     def __init__(self, title, authors):
...         self.title = title
...         self.authors = authors
...
>>> class AuthorMapper(Mapper):
...     author = RawField("name")
...
>>> class BookMapper(Mapper):
...     title = RawField()
...     authors = ListDelegateField(AuthorMapper)
...
>>> book = Book("be clound", [Person("bucho"), Person("shacho")])
>>> print(BookMapper(book).as_dict())
OrderedDict([('title', 'be clound'), ('authors', [OrderedDict([('author', 'bucho')]), OrderedDict([('author', 'shacho')])])])
>>> def get_vals(items):
...     """
...     辞書のリストから、値だけを取り出す関数
...
...     >>> get_vals([{"pt":1}, {"pt":2}])
...     [1, 2]
...     """
...     result = []
...     for dic in items:
...         for k, v in dic.items():
...             result.append(v)
...     return result
...
>>> class BookMapperExt(Mapper):
...     title = RawField()
...     authors = ListDelegateField(AuthorMapper, after_callback=get_vals)
...
>>> book = Book("be clound", [Person("bucho"), Person("shacho")])
>>> print(BookMapperExt(book).as_dict())
OrderedDict([('title', 'be clound'), ('authors', ['bucho', 'shacho'])])

Note

filter_FOO, after_filter_FOO, callback, after_callbackは以下の順序で呼ばれます。

  1. filter_FOO
  2. callback
  3. after_callback
  4. after_filter_FOO

実行例

>>> from bpmappers import Mapper, RawField, DelegateField
>>> class Person(object):
...     def __init__(self, name):
...         self.name = name
...
>>> class PersonInfoMapper(Mapper):
...     info = RawField("name",
...                     callback= lambda v :  "( cb: %s )" % v,
...                     after_callback = lambda v :  "[ after_cb: %s ]" % v)
...
...     def filter_info(self, v):
...         return "< filter: %s >" % v
...
...     def after_filter_info(self, v):
...         return "{ after_filter: %s }" % v
...
>>> mapper = PersonInfoMapper(Person("BP"))
>>> print(mapper.as_dict())
OrderedDict([('info', '{ after_filter: [ after_cb: ( cb: < filter: BP > ) ] }')])

Mapper.key_name

キー名を変更したい場合などに使用します。

>>> from bpmappers import Mapper, RawField
>>> class NameSpaceMapper(Mapper):
...     name = RawField()
...     def key_name(self, name,  value, field):
...         return 'namespace:%s' % name
...
>>> NameSpaceMapper(dict(name='bucho')).as_dict()
OrderedDict([('namespace:name', 'bucho')])

同じ階層にマッピング結果をマージする

myapp/models.py に定義した次のようなPersonモデルとBookモデルを同じ階層にマッピングする例です。

from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=20)
    age = models.PositiveIntegerField()

    def __unicode__(self):
        return '%s, %d' % (self.name, self.age)

class Book(models.Model):
    title = models.CharField(max_length=30)
    author = models.ForeignKey(Person)

    def __unicode__(self):
        return '%s, %s' % (self.title, self.author)

myapp/mappers.py にマッピングルールを定義します。 DelegateFieldattach_parent オプションに True を指定することで、対象のマッピング結果を同じ階層のマッピング結果にマージします。

from bpmappers import Mapper, fields, djangomodel
from myapp.models import Person, Book

class PersonModelMapper(djangomodel.ModelMapper):
    class Meta:
        model = Person
        exclude = ['id']

class BookFlattenMapper(djangomodel.ModelMapper):
    author = fields.DelegateField(PersonModelMapper, attach_parent=True)

    class Meta:
        model = Book

結果はこのようになります:

>>> from myapp.models import Book
>>> book = Book.objects.get(pk=1)
>>> book
<Book: feizbook, feiz, 23>
>>> from myapp.mappers import BookFlattenMapper
>>> BookFlattenMapper(book).as_dict()
{'name': u'feiz', 'age': 23, 'id': 1, 'title': u'feizbook'}

Djangoフレームワークのモデルをマッピングする

Djangoフレームワークのモデルインスタンスをマッピングする場合、 bpmappers.djangomodel.ModelMapper を使用することができます。

Djangoフレームワークのバージョン

bpmappers.djangomodel は、Djangoフレームワークのバージョン 1.8 以上に対応しています。

ModelMapperの使用

ModelMapperを使用するには、 ModelMapper を継承したクラスを定義します。

Djangoで次のようなモデルを定義したとします:

from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=10)

class Book(models.Model):
    title = models.CharField(max_length=10)
    author = models.Foreignkey(Person)

ModelMapperを使ってBookモデルを辞書にマッピングするための定義は次のようになります:

from bpmappers.djangomodel import ModelMapper
from myapp.models import Book

class BookMapper(ModelMapper):
    class Meta:
        model = Book

ModelMapperを使わない場合は次のようになります:

from bpmappers import Mapper, RawField, DelegateField

class PersonMapper(Mapper):
    id = RawField()
    name = RawField()

class BookMapper(Mapper):
    id = RawField()
    title = RawField()
    author = DelegateField(PersonMapper)

仕組み

ModelMapperは、Djangoモデルクラスのメタ情報(Model._meta.fields)を参照してマッピング定義を作成しています。

Djangoのモデルフィールドとbpmppersのフィールドの対応は次の通りです:

Djangoのモデルフィールド bpmappersのフィールド
AutoField bpmappers.RawField
CharField bpmappers.RawField
TextField bpmappers.RawField
IntegerField bpmappers.RawField
DateTimeField bpmappers.RawField
DateField bpmappers.RawField
TimeField bpmappers.RawField
BooleanField bpmappers.RawField
FileField bpmappers.djangomodel.DjangoFileField
ForeignKey bpmappers.DelegateField
ManyToManyField bpmappers.ListDelegateField

Metaインナークラス

ModelMapper を継承したクラスには、 Meta インナークラスを定義する必要があります。このクラスで定義した内容から、マッピングルールが生成されます。

Meta.model

Djangoのモデルクラスを指定します。

Meta.fields

Meta.model で指定したモデルクラスのフィールドのうち、マッピング対象とするフィールド名をシーケンス型で列挙します。省略した場合はすべてのフィールドがマッピング対象になります。

Meta.exclude

Meta.model で指定したモデルクラスのフィールドのうち、マッピング対象から除外するフィールド名をシーケンス型で列挙します。省略した場合は、すべてのフィールドがマッピング対象になります。

リファレンス

bpmappers.djangomodel

class bpmappers.djangomodel.DjangoFileField(key=None, callback=None, skip_callable=False, *args, **kwargs)
exception bpmappers.djangomodel.MetaModelError

Invalid mapper Meta

class bpmappers.djangomodel.ModelMapper(data=None, **options)

Mapper class for Django ORM.

This class generates mapping definition with using Django Model’s Meta
data.
class bpmappers.djangomodel.ModelMapperMetaclass

bpmappers.exceptions

exception bpmappers.exceptions.DataError(message)
exception bpmappers.exceptions.InvalidDelegateException(message)

bpmappers.fields

class bpmappers.fields.ChoiceField(choices, key=None, callback=None, skip_callable=False, *args, **kwargs)
class bpmappers.fields.DelegateField(mapper_class, key=None, callback=None, skip_callable=True, before_filter=None, required=True, attach_parent=False, *args, **kwargs)

It is Field delegating mapping to the mapper_class.

class bpmappers.fields.Field(key=None, callback=None, skip_callable=False, *args, **kwargs)

Basic class of Field.

class bpmappers.fields.ListDelegateField(mapper_class, key=None, callback=None, filter=None, skip_callable=True, after_filter=None, *args, **kwargs)

Delegate mapping to mapper_class the value as list.

class bpmappers.fields.NonKeyDelegateField(mapper_class, callback=None, attach_parent=False, *args, **kwargs)
class bpmappers.fields.NonKeyField(callback=None, after_callback=None, *args, **kwargs)

Result values are generated manually.

class bpmappers.fields.NonKeyListDelegateField(mapper_class, callback=None, filter=None, after_filter=None, *args, **kwargs)
class bpmappers.fields.RawField(key=None, callback=None, skip_callable=False, *args, **kwargs)

Result values are obtained from mapping source without conversion.

class bpmappers.fields.StubField(stub={}, *args, **kwargs)

Result values are fixed value.

bpmappers.mappers

class bpmappers.mappers.BaseMapper

Metaclass of Mapper.

class bpmappers.mappers.Mapper(data=None, **options)

Basic Mapper class.

as_dict()

Return the OrderedDict it is mapping result.

key_name(name, value, field)

Hook point for key name converting.

order(parsed)

This method must return the OrderedDict.

class bpmappers.mappers.Options(*args, **kwargs)

Meta data of Mapper.

add_field(name, field)

Add field

bpmappers.utils

bpmappers.utils.sort_dict_with_keys(target_dict, keys)

Sorting target_dict with keys list.

Returns:OrderedDict

Change History

1.0.1

  • Modify documentation config

1.0

  • Support Python 3.7
  • Support Django 2.0, 2.1
  • Remove support old versions of Django <1.11.

0.9

  • Rewrote documentation.
  • Change testing framework to pytest.
  • Removed support old versions of Python <2.7, <3.4.
  • Removed support old versions of Django <1.8.
  • Removed SortedDict, mapping result uses OrderedDict.
  • Removed MultiValueDict, replaced to defaultdict.

0.8.2

  • Modify documentation

0.8.1

  • Supports Django 1.8
  • fixes #25

0.8

  • Supports Django 1.7

0.7

  • Removed the six, and added it to required module.
  • Support Python3 in djangomodel
  • Removed support Python 3.1

用語集

マッピング
複数の辞書の値やオブジェクトのプロパティを別の辞書に代入する操作。
マッピングクラス
bpmappers.Mapper を継承したクラス。マッピングに使用します。
マッピングソース
マッピング時に、値を取得する対象となるオブジェクト。
マッピング結果
マッピングを実行して得られる結果のOrderedDict。
フィールドクラス
マッピングクラスでフィールドを定義する際に使うクラス。マッピング結果として使う値の取得、生成を制御します。