diff --git a/rattail_nationbuilder/commands.py b/rattail_nationbuilder/commands.py new file mode 100644 index 0000000..de3456a --- /dev/null +++ b/rattail_nationbuilder/commands.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2023 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +rattail-nationbuilder commands +""" + +from rattail import commands + + +class ImportNationBuilder(commands.ImportSubcommand): + """ + Import data for NationBuilder => Rattail + """ + name = 'import-nationbuilder' + description = __doc__.strip() + handler_key = 'to_rattail.from_nationbuilder.import' diff --git a/rattail_nationbuilder/config.py b/rattail_nationbuilder/config.py new file mode 100644 index 0000000..de2aef3 --- /dev/null +++ b/rattail_nationbuilder/config.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2023 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +Config Extension +""" + +from rattail.config import ConfigExtension + + +class RattailNationBuilderExtension(ConfigExtension): + """ + Config extension for rattail-nationbuilder + """ + key = 'rattail_nationbuilder' + + def configure(self, config): + + # rattail import-nationbuilder + config.setdefault('rattail.importing', 'to_rattail.from_nationbulder.import.default_handler', + 'rattail_nationbuilder.importing.nationbuilder:FromNationBuilderToRattail') + config.setdefault('rattail.importing', 'to_rattail.from_nationbuilder.import.default_cmd', + 'rattail import-nationbuilder') diff --git a/rattail_nationbuilder/db/__init__.py b/rattail_nationbuilder/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rattail_nationbuilder/db/alembic/versions/1e17031c4b3e_first_cache_tables.py b/rattail_nationbuilder/db/alembic/versions/1e17031c4b3e_first_cache_tables.py new file mode 100644 index 0000000..cf11eb0 --- /dev/null +++ b/rattail_nationbuilder/db/alembic/versions/1e17031c4b3e_first_cache_tables.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8; -*- +"""first cache tables + +Revision ID: 1e17031c4b3e +Revises: fa3aec1556bc +Create Date: 2023-09-12 15:05:08.853989 + +""" + +# revision identifiers, used by Alembic. +revision = '1e17031c4b3e' +down_revision = None +branch_labels = ('rattail_nationbuilder',) +depends_on = None + +from alembic import op +import sqlalchemy as sa +import rattail.db.types + + + +def upgrade(): + + # nationbuilder_cache_person + op.create_table('nationbuilder_cache_person', + sa.Column('uuid', sa.String(length=32), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('email', sa.String(length=255), nullable=True), + sa.Column('email_opt_in', sa.Boolean(), nullable=True), + sa.Column('external_id', sa.String(length=50), nullable=True), + sa.Column('first_name', sa.String(length=100), nullable=True), + sa.Column('middle_name', sa.String(length=100), nullable=True), + sa.Column('last_name', sa.String(length=100), nullable=True), + sa.Column('mobile', sa.String(length=50), nullable=True), + sa.Column('mobile_opt_in', sa.Boolean(), nullable=True), + sa.Column('note', sa.Text(), nullable=True), + sa.Column('phone', sa.String(length=50), nullable=True), + sa.Column('primary_image_url_ssl', sa.String(length=255), nullable=True), + sa.Column('signup_type', sa.Integer(), nullable=True), + sa.Column('primary_address_address1', sa.String(length=100), nullable=True), + sa.Column('primary_address_address2', sa.String(length=100), nullable=True), + sa.Column('primary_address_city', sa.String(length=100), nullable=True), + sa.Column('primary_address_state', sa.String(length=50), nullable=True), + sa.Column('primary_address_zip', sa.String(length=10), nullable=True), + sa.Column('tags', sa.Text(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('uuid'), + sa.UniqueConstraint('id', name='nationbuilder_cache_person_uq_id') + ) + op.create_table('nationbuilder_cache_person_version', + sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False), + sa.Column('id', sa.Integer(), autoincrement=False, nullable=True), + sa.Column('created_at', sa.DateTime(), autoincrement=False, nullable=True), + sa.Column('email', sa.String(length=255), autoincrement=False, nullable=True), + sa.Column('email_opt_in', sa.Boolean(), autoincrement=False, nullable=True), + sa.Column('external_id', sa.String(length=50), autoincrement=False, nullable=True), + sa.Column('first_name', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('middle_name', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('last_name', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('mobile', sa.String(length=50), autoincrement=False, nullable=True), + sa.Column('mobile_opt_in', sa.Boolean(), autoincrement=False, nullable=True), + sa.Column('note', sa.Text(), autoincrement=False, nullable=True), + sa.Column('phone', sa.String(length=50), autoincrement=False, nullable=True), + sa.Column('primary_image_url_ssl', sa.String(length=255), autoincrement=False, nullable=True), + sa.Column('signup_type', sa.Integer(), autoincrement=False, nullable=True), + sa.Column('primary_address_address1', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('primary_address_address2', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('primary_address_city', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('primary_address_state', sa.String(length=50), autoincrement=False, nullable=True), + sa.Column('primary_address_zip', sa.String(length=10), autoincrement=False, nullable=True), + sa.Column('tags', sa.Text(), autoincrement=False, nullable=True), + sa.Column('updated_at', sa.DateTime(), autoincrement=False, nullable=True), + sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False), + sa.Column('end_transaction_id', sa.BigInteger(), nullable=True), + sa.Column('operation_type', sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint('uuid', 'transaction_id') + ) + op.create_index(op.f('ix_nationbuilder_cache_person_version_end_transaction_id'), 'nationbuilder_cache_person_version', ['end_transaction_id'], unique=False) + op.create_index(op.f('ix_nationbuilder_cache_person_version_operation_type'), 'nationbuilder_cache_person_version', ['operation_type'], unique=False) + op.create_index(op.f('ix_nationbuilder_cache_person_version_transaction_id'), 'nationbuilder_cache_person_version', ['transaction_id'], unique=False) + + +def downgrade(): + + # nationbuilder_cache_person + op.drop_index(op.f('ix_nationbuilder_cache_person_version_transaction_id'), table_name='nationbuilder_cache_person_version') + op.drop_index(op.f('ix_nationbuilder_cache_person_version_operation_type'), table_name='nationbuilder_cache_person_version') + op.drop_index(op.f('ix_nationbuilder_cache_person_version_end_transaction_id'), table_name='nationbuilder_cache_person_version') + op.drop_table('nationbuilder_cache_person_version') + op.drop_table('nationbuilder_cache_person') diff --git a/rattail_nationbuilder/db/model/__init__.py b/rattail_nationbuilder/db/model/__init__.py new file mode 100644 index 0000000..9499a43 --- /dev/null +++ b/rattail_nationbuilder/db/model/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2023 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +DB schema for NationBuilder integration +""" + +from .nationbuilder import NationBuilderCachePerson diff --git a/rattail_nationbuilder/db/model/nationbuilder.py b/rattail_nationbuilder/db/model/nationbuilder.py new file mode 100644 index 0000000..ad9b03f --- /dev/null +++ b/rattail_nationbuilder/db/model/nationbuilder.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2023 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +NationBuilder cache tables +""" + +import sqlalchemy as sa +from sqlalchemy import orm + +from rattail.db import model +from rattail.db.util import normalize_full_name +from rattail.config import parse_list + + +class NationBuilderCachePerson(model.Base): + """ + Represents a Person record in NationBuilder. + + https://apiexplorer.nationbuilder.com/nationbuilder#People + """ + __tablename__ = 'nationbuilder_cache_person' + __table_args__ = ( + sa.UniqueConstraint('id', name='nationbuilder_cache_person_uq_id'), + ) + __versioned__ = {} + model_title = "NationBuilder Person" + model_title_plural = "NationBuilder People" + + uuid = model.uuid_column() + + id = sa.Column(sa.Integer(), nullable=False) + signup_type = sa.Column(sa.Integer(), nullable=True) + external_id = sa.Column(sa.String(length=50), nullable=True) + tags = sa.Column(sa.String(length=255), nullable=True) + first_name = sa.Column(sa.String(length=100), nullable=True) + middle_name = sa.Column(sa.String(length=100), nullable=True) + last_name = sa.Column(sa.String(length=100), nullable=True) + email = sa.Column(sa.String(length=255), nullable=True) + email_opt_in = sa.Column(sa.Boolean(), nullable=True) + mobile = sa.Column(sa.String(length=50), nullable=True) + mobile_opt_in = sa.Column(sa.Boolean(), nullable=True) + phone = sa.Column(sa.String(length=50), nullable=True) + primary_address_address1 = sa.Column(sa.String(length=100), nullable=True) + primary_address_address2 = sa.Column(sa.String(length=100), nullable=True) + primary_address_city = sa.Column(sa.String(length=100), nullable=True) + primary_address_state = sa.Column(sa.String(length=50), nullable=True) + primary_address_zip = sa.Column(sa.String(length=10), nullable=True) + primary_image_url_ssl = sa.Column(sa.String(length=255), nullable=True) + note = sa.Column(sa.Text(), nullable=True) + created_at = sa.Column(sa.DateTime(), nullable=True) + updated_at = sa.Column(sa.DateTime(), nullable=True) + + def __str__(self): + return normalize_full_name(self.first_name, self.last_name) + + def has_tag(self, tag): + if self.tags: + for value in parse_list(self.tags): + if value == tag: + return True + return False diff --git a/rattail_nationbuilder/importing/__init__.py b/rattail_nationbuilder/importing/__init__.py new file mode 100644 index 0000000..2be12fb --- /dev/null +++ b/rattail_nationbuilder/importing/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2023 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +rattail-nationbuilder importing +""" + +from . import model diff --git a/rattail_nationbuilder/importing/model.py b/rattail_nationbuilder/importing/model.py new file mode 100644 index 0000000..c0ac289 --- /dev/null +++ b/rattail_nationbuilder/importing/model.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2023 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +rattail-nationbuilder model importers +""" + +from rattail.importing.model import ToRattail +from rattail_nationbuilder.db import model + + +############################## +# nationbuilder cache +############################## + +class NationBuilderCachePersonImporter(ToRattail): + model_class = model.NationBuilderCachePerson diff --git a/rattail_nationbuilder/importing/nationbuilder.py b/rattail_nationbuilder/importing/nationbuilder.py new file mode 100644 index 0000000..2507f86 --- /dev/null +++ b/rattail_nationbuilder/importing/nationbuilder.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2023 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +NationBuilder -> Rattail importing +""" + +import datetime +from collections import OrderedDict + +from rattail import importing +from rattail_nationbuilder import importing as nationbuilder_importing +from rattail_nationbuilder.nationbuilder.webapi import NationBuilderWebAPI + + +class FromNationBuilderToRattail(importing.ToRattailHandler): + """ + Import handler for NationBuilder -> Rattail + """ + host_key = 'nationbuilder' + host_title = "NationBuilder" + generic_host_title = "NationBuilder" + + def get_importers(self): + importers = OrderedDict() + importers['NationBuilderCachePerson'] = NationBuilderCachePersonImporter + return importers + + +class FromNationBuilder(importing.Importer): + """ + Base class for all NationBuilder importers + """ + + def setup(self): + super().setup() + self.setup_api() + + def setup_api(self): + self.nationbuilder = NationBuilderWebAPI(self.config) + + +class NationBuilderCachePersonImporter(FromNationBuilder, nationbuilder_importing.model.NationBuilderCachePersonImporter): + """ + Importer for NB Person cache + """ + key = 'id' + + primary_address_fields = [ + 'primary_address_address1', + 'primary_address_address2', + 'primary_address_city', + 'primary_address_state', + 'primary_address_zip', + ] + + supported_fields = [ + 'id', + 'created_at', + 'email', + 'email_opt_in', + 'external_id', + 'first_name', + 'middle_name', + 'last_name', + 'mobile', + 'mobile_opt_in', + 'note', + 'phone', + 'primary_image_url_ssl', + 'signup_type', + 'tags', + 'updated_at', + ] + primary_address_fields + + def get_host_objects(self): + return self.nationbuilder.get_people(page_size=500) + + def normalize_timestamp(self, value): + if not value: + return + + dt = datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S%z') + dt = self.app.localtime(dt) + return self.app.make_utc(dt) + + def normalize_host_object(self, person): + + # nb. some fields may not be present in person dict + data = dict([(field, person.get(field)) + for field in self.fields]) + if data: + + for field in ('created_at', 'updated_at'): + data[field] = self.normalize_timestamp(data[field]) + + if 'tags' in self.fields: + tags = data['tags'] + if tags: + data['tags'] = self.config.make_list_string(tags) + + if self.fields_active(self.primary_address_fields): + address = person.get('primary_address') + if address: + data.update({ + 'primary_address_address1': address['address1'], + 'primary_address_address2': address['address2'], + 'primary_address_state': address['state'], + 'primary_address_city': address['city'], + 'primary_address_zip': address['zip'], + }) + + return data diff --git a/rattail_nationbuilder/importing/versions.py b/rattail_nationbuilder/importing/versions.py new file mode 100644 index 0000000..414c912 --- /dev/null +++ b/rattail_nationbuilder/importing/versions.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2023 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +rattail-nationbuilder version importing +""" + +from rattail.importing import versions as base + + +class NationBuilderVersionMixin(object): + + def add_nationbuilder_importers(self, importers): + importers['NationBuilderCachePerson'] = NationBuilderCachePersonImporter + return importers + + +class NationBuilderCachePersonImporter(base.VersionImporter): + + @property + def host_model_class(self): + return self.model.NationBuilderCachePerson diff --git a/rattail_nationbuilder/nationbuilder/util.py b/rattail_nationbuilder/nationbuilder/util.py new file mode 100644 index 0000000..2701122 --- /dev/null +++ b/rattail_nationbuilder/nationbuilder/util.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2023 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +NationBuilder utils +""" + + +def get_nationbuilder_url(config): + url = config.get('nationbuilder', 'url') + if url: + return url.rstrip('/') diff --git a/rattail_nationbuilder/nationbuilder/webapi.py b/rattail_nationbuilder/nationbuilder/webapi.py index 9518e46..019db08 100644 --- a/rattail_nationbuilder/nationbuilder/webapi.py +++ b/rattail_nationbuilder/nationbuilder/webapi.py @@ -89,7 +89,7 @@ class NationBuilderWebAPI(object): """ return self._request('GET', api_method, params=params) - def get_people(self, page_size=10, progress=None, **kwargs): + def get_people(self, page_size=10, max_pages=None, progress=None, **kwargs): """ Retrieve all Person records. @@ -109,8 +109,12 @@ class NationBuilderWebAPI(object): data = response.json() people.extend(data['results']) url['next'] = data['next'] + log.debug("have fetched %s pages", page + 1) - self.app.progress_loop(fetch, range(pages), progress, + pages = list(range(pages)) + if max_pages: + pages = pages[:max_pages] + self.app.progress_loop(fetch, pages, progress, message="Fetching Person data from NationBuilder") return people diff --git a/setup.cfg b/setup.cfg index 6a0a95e..17bf32c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,3 +27,15 @@ install_requires = packages = find: include_package_data = True + + +[options.entry_points] + +rattail.commands = + import-nationbuilder = rattail_nationbuilder.commands:ImportNationBuilder + +rattail.config.extensions = + rattail_nationbuilder = rattail_nationbuilder.config:RattailNationBuilderExtension + +rattail.importing = + to_rattail.from_nationbuilder.import = rattail_nationbuilder.importing.nationbuilder:FromNationBuilderToRattail