Compare commits

...

45 commits

Author SHA1 Message Date
Lance Edgar 5a6c89589e docs: update project links, kallithea -> forgejo 2024-09-14 12:10:25 -05:00
Lance Edgar 785b32c5f0 bump: version 0.3.3 → 0.3.4 2024-08-19 12:03:21 -05:00
Lance Edgar d1d181bb43 fix: avoid deprecated method in app provider 2024-08-19 09:55:54 -05:00
Lance Edgar 253791134a fix: avoid deprecated base class for config extension 2024-08-19 09:42:17 -05:00
Lance Edgar edbe306bdf bump: version 0.3.2 → 0.3.3 2024-08-19 09:38:45 -05:00
Lance Edgar dfe820455b fix: avoid deprecated import for parse_list() 2024-08-14 11:16:28 -05:00
Lance Edgar 69e2720e93 bump: version 0.3.1 → 0.3.2 2024-08-13 11:25:20 -05:00
Lance Edgar d44c693080 fix: update app provider entry point, per wuttjamaican 2024-07-14 12:46:08 -05:00
Lance Edgar 46c07567fe bump: version 0.3.0 → 0.3.1 2024-07-01 14:15:10 -05:00
Lance Edgar adbf48ba57 fix: remove legacy command definitions 2024-07-01 12:24:21 -05:00
Lance Edgar d503de44a2 bump: version 0.2.0 → 0.3.0 2024-06-10 22:52:27 -05:00
Lance Edgar 3216d27359 feat: switch from setup.cfg to pyproject.toml + hatchling 2024-06-10 22:52:09 -05:00
Lance Edgar 55a7c4a9be Fix default dist filename for release task
not sure why this fix was needed, did setuptools behavior change?
2024-05-29 10:15:24 -05:00
Lance Edgar 0e0823e043 Update changelog 2024-05-29 10:14:17 -05:00
Lance Edgar b04816b1ef Add typer equivalents for rattail commands 2024-05-16 20:13:01 -05:00
Lance Edgar 6612c33b9e Update changelog 2023-12-01 07:06:51 -06:00
Lance Edgar 7e3c399788 Update subcommand entry point group names, per wuttjamaican 2023-12-01 07:06:28 -06:00
Lance Edgar 90efc7b945 Update changelog 2023-10-16 11:14:14 -05:00
Lance Edgar 6cd71ea4c2 Limit page size to 100, for fetching Person records from NB API
apparently that is the effective limit..so must honor it
2023-10-16 11:12:51 -05:00
Lance Edgar a7071d140b Update changelog 2023-09-15 10:41:06 -05:00
Lance Edgar 8e4b5a2971 Add rattail provider for NationBuilder integration
and use it to generate NB URLs
2023-09-14 13:21:35 -05:00
Lance Edgar eb6bcf8673 Update changelog 2023-09-13 13:18:47 -05:00
Lance Edgar d6a0d3b090 Fix schema inconsistencies 2023-09-13 09:18:45 -05:00
Lance Edgar 0d9f0d1daa Update changelog 2023-09-12 21:45:13 -05:00
Lance Edgar 2f47bbff9b Assume null if NB person tags are empty 2023-09-12 21:33:41 -05:00
Lance Edgar 20a16d5d9d Update changelog 2023-09-12 20:52:27 -05:00
Lance Edgar 63286679ad Add cache table, importer for NationBuilder donations 2023-09-12 20:49:18 -05:00
Lance Edgar 719de78413 Update changelog 2023-09-12 18:56:06 -05:00
Lance Edgar e46df76a42 Fix manifest again..omg 2023-09-12 18:55:43 -05:00
Lance Edgar 2cc9dd0843 Update changelog 2023-09-12 18:50:30 -05:00
Lance Edgar 8a50e14e2c Fix manifest...omg 2023-09-12 18:50:04 -05:00
Lance Edgar be5ab23128 Update changelog 2023-09-12 18:45:02 -05:00
Lance Edgar 75186b7319 Add alembic scripts to manifest 2023-09-12 18:44:24 -05:00
Lance Edgar 4af7ec800c Update changelog 2023-09-12 18:33:44 -05:00
Lance Edgar c3b93edd4d Add cache table, importer for NationBuilder People 2023-09-12 18:15:23 -05:00
Lance Edgar 93af35eb4e Update changelog 2023-09-07 21:08:12 -05:00
Lance Edgar 80f8608340 Add web API methods for fetching donations from NationBuilder 2023-09-07 21:07:31 -05:00
Lance Edgar 49e07f04d9 Update changelog 2023-05-25 21:37:18 -05:00
Lance Edgar ebf1e6eb60 Should actually use requests session for web api... 2023-05-25 21:36:35 -05:00
Lance Edgar baadb86d5d Update changelog 2023-05-17 07:01:42 -05:00
Lance Edgar 22306c2de6 Replace setup.py contents with setup.cfg 2023-05-17 07:01:16 -05:00
Lance Edgar de7e9cbac9 Update changelog 2023-05-11 15:25:21 -05:00
Lance Edgar 5438db9f27 Add max_pages arg for API get_people_with_tag() method
for sake of testing, limit how many results to fetch
2023-05-09 20:26:45 -05:00
Lance Edgar dfbd8f1652 Add max_retries config for NationBuilder API
b/c a particular install has frequent DNS issues overnight...
2023-05-09 12:23:52 -05:00
Lance Edgar 9fe89f0190 Upload to PyPI for release task 2023-05-08 15:36:02 -05:00
22 changed files with 1090 additions and 92 deletions

3
.gitignore vendored
View file

@ -1 +1,4 @@
*~
*.pyc
dist/
rattail_nationbuilder.egg-info/

View file

@ -5,6 +5,98 @@ All notable changes to rattail-nationbuilder will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## v0.3.4 (2024-08-19)
### Fix
- avoid deprecated method in app provider
- avoid deprecated base class for config extension
## v0.3.3 (2024-08-19)
### Fix
- avoid deprecated import for `parse_list()`
## v0.3.2 (2024-08-13)
### Fix
- update app provider entry point, per wuttjamaican
## v0.3.1 (2024-07-01)
### Fix
- remove legacy command definitions
## v0.3.0 (2024-06-10)
### Feat
- switch from setup.cfg to pyproject.toml + hatchling
## [0.2.0] - 2024-05-29
### Changed
- Migrate all commands to use `typer`.
## [0.1.14] - 2023-12-01
### Changed
- Update subcommand entry point group names, per wuttjamaican.
## [0.1.13] - 2023-09-16
### Changed
- Limit page size to 100, for fetching Person records from NB API.
## [0.1.12] - 2023-09-15
### Changed
- Add rattail provider for NationBuilder integration.
## [0.1.11] - 2023-09-13
### Changed
- Fix schema inconsistencies.
## [0.1.10] - 2023-09-12
### Changed
- Assume null if NB person tags are empty.
## [0.1.9] - 2023-09-12
### Changed
- Add cache table, importer for NationBuilder donations.
## [0.1.8] - 2023-09-12
### Changed
- Fix manifest again..omg.
## [0.1.7] - 2023-09-12
### Changed
- Fix manifest...omg.
## [0.1.6] - 2023-09-12
### Changed
- Add alembic scripts to manifest.
## [0.1.5] - 2023-09-12
### Changed
- Add cache table, importer for NationBuilder People.
## [0.1.4] - 2023-09-07
### Changed
- Add web API methods for fetching donations from NationBuilder.
## [0.1.3] - 2023-05-25
### Changed
- Should actually use requests session for web api.
## [0.1.2] - 2023-05-17
### Changed
- Replace `setup.py` contents with `setup.cfg`.
## [0.1.1] - 2023-05-11
### Changed
- Add `max_retries` config for NationBuilder API.
- Add `max_pages` arg for API `get_people_with_tag()` method.
## [0.1.0] - 2023-05-08
### Added
- Initial version, basic API client only.

View file

@ -1,2 +1,3 @@
include *.md
include *.rst
recursive-include rattail_nationbuilder/db/alembic *.py

55
pyproject.toml Normal file
View file

@ -0,0 +1,55 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "rattail-nationbuilder"
version = "0.3.4"
description = "Rattail integration package for NationBuilder"
readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
license = {text = "GNU GPL v3+"}
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Topic :: Office/Business",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"rattail",
]
[project.entry-points."rattail.typer_imports"]
rattail_nationbuilder = "rattail_nationbuilder.commands"
[project.entry-points."rattail.config.extensions"]
rattail_nationbuilder = "rattail_nationbuilder.config:RattailNationBuilderExtension"
[project.entry-points."rattail.importing"]
"to_rattail.from_nationbuilder.import" = "rattail_nationbuilder.importing.nationbuilder:FromNationBuilderToRattail"
[project.entry-points."wutta.app.providers"]
rattail_nationbuilder = "rattail_nationbuilder.app:NationBuilderProvider"
[project.urls]
Homepage = "https://rattailproject.org"
Repository = "https://forgejo.wuttaproject.org/rattail/rattail-nationbuilder"
Changelog = "https://forgejo.wuttaproject.org/rattail/rattail-nationbuilder/src/branch/master/CHANGELOG.md"
[tool.commitizen]
version_provider = "pep621"
tag_format = "v$version"
update_changelog_on_bump = true

View file

@ -1,3 +1,6 @@
# -*- coding: utf-8; -*-
__version__ = '0.1.0'
from importlib.metadata import version
__version__ = version('rattail-nationbuilder')

View file

@ -0,0 +1,56 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2024 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
App Handler supplement
"""
from rattail.app import RattailProvider, GenericHandler
class NationBuilderProvider(RattailProvider):
"""
App provider for NationBuilder integration.
"""
def get_nationbuilder_handler(self, **kwargs):
if 'nationbuilder' not in self.handlers:
spec = self.config.get('rattail', 'nationbuilder.handler',
default='rattail_nationbuilder.app:NationBuilderHandler')
factory = self.app.load_object(spec)
self.handlers['nationbuilder'] = factory(self.config, **kwargs)
return self.handlers['nationbuilder']
class NationBuilderHandler(GenericHandler):
"""
Handler for NationBuilder integration.
"""
def get_url(self, require=False, **kwargs):
"""
Returns the base URL for the NationBuilder web app.
"""
getter = self.config.require if require else self.config.get
url = getter('nationbuilder', 'url')
if url:
return url.rstrip('/')

View file

@ -0,0 +1,48 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2024 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
rattail-nationbuilder commands
"""
import typer
from rattail.commands import rattail_typer
from rattail.commands.typer import importer_command, typer_get_runas_user
from rattail.commands.importing import ImportCommandHandler
@rattail_typer.command()
@importer_command
def import_nationbuilder(
ctx: typer.Context,
**kwargs
):
"""
Import data for NationBuilder => Rattail
"""
config = ctx.parent.rattail_config
progress = ctx.parent.rattail_progress
handler = ImportCommandHandler(
config, import_handler_key='to_rattail.from_nationbuilder.import')
kwargs['user'] = typer_get_runas_user(ctx)
handler.run(kwargs, progress=progress)

View file

@ -0,0 +1,42 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2024 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Config Extension
"""
from wuttjamaican.conf import WuttaConfigExtension
class RattailNationBuilderExtension(WuttaConfigExtension):
"""
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')

View file

View file

@ -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')

View file

@ -0,0 +1,37 @@
# -*- coding: utf-8; -*-
"""fix nullable
Revision ID: 7fc3dee0e9c5
Revises: c3cb75afcae2
Create Date: 2023-09-13 09:12:33.740638
"""
# revision identifiers, used by Alembic.
revision = '7fc3dee0e9c5'
down_revision = 'c3cb75afcae2'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
import rattail.db.types
def upgrade():
# nationbuilder_cache_donation
op.alter_column('nationbuilder_cache_donation_version', 'id',
existing_type=sa.INTEGER(),
nullable=True,
autoincrement=False)
def downgrade():
# nationbuilder_cache_donation
op.alter_column('nationbuilder_cache_donation_version', 'id',
existing_type=sa.INTEGER(),
nullable=False,
autoincrement=False)

View file

@ -0,0 +1,81 @@
# -*- coding: utf-8; -*-
"""add donations cache table
Revision ID: c3cb75afcae2
Revises: 1e17031c4b3e
Create Date: 2023-09-12 19:30:47.583505
"""
# revision identifiers, used by Alembic.
revision = 'c3cb75afcae2'
down_revision = '1e17031c4b3e'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
import rattail.db.types
def upgrade():
# nationbuilder_cache_donation
op.create_table('nationbuilder_cache_donation',
sa.Column('uuid', sa.String(length=32), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('author_id', sa.Integer(), nullable=True),
sa.Column('membership_id', sa.Integer(), nullable=True),
sa.Column('donor_id', sa.Integer(), nullable=True),
sa.Column('donor_external_id', sa.String(length=50), nullable=True),
sa.Column('email', sa.String(length=255), nullable=True),
sa.Column('amount', sa.Numeric(precision=10, scale=2), nullable=True),
sa.Column('payment_type_name', sa.String(length=100), nullable=True),
sa.Column('check_number', sa.String(length=255), nullable=True),
sa.Column('tracking_code_slug', sa.String(length=255), nullable=True),
sa.Column('note', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('succeeded_at', sa.DateTime(), nullable=True),
sa.Column('failed_at', sa.DateTime(), nullable=True),
sa.Column('canceled_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('uuid'),
sa.UniqueConstraint('id', name='nationbuilder_cache_donation_uq_id')
)
op.create_table('nationbuilder_cache_donation_version',
sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False),
sa.Column('id', sa.Integer(), autoincrement=False, nullable=False),
sa.Column('author_id', sa.Integer(), nullable=True),
sa.Column('membership_id', sa.Integer(), nullable=True),
sa.Column('donor_id', sa.Integer(), nullable=True),
sa.Column('donor_external_id', sa.String(length=50), nullable=True),
sa.Column('email', sa.String(length=255), nullable=True),
sa.Column('amount', sa.Numeric(precision=10, scale=2), nullable=True),
sa.Column('payment_type_name', sa.String(length=100), nullable=True),
sa.Column('check_number', sa.String(length=255), nullable=True),
sa.Column('tracking_code_slug', sa.String(length=255), nullable=True),
sa.Column('note', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('succeeded_at', sa.DateTime(), nullable=True),
sa.Column('failed_at', sa.DateTime(), nullable=True),
sa.Column('canceled_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), 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_donation_version_end_transaction_id'), 'nationbuilder_cache_donation_version', ['end_transaction_id'], unique=False)
op.create_index(op.f('ix_nationbuilder_cache_donation_version_operation_type'), 'nationbuilder_cache_donation_version', ['operation_type'], unique=False)
op.create_index(op.f('ix_nationbuilder_cache_donation_version_transaction_id'), 'nationbuilder_cache_donation_version', ['transaction_id'], unique=False)
def downgrade():
# nationbuilder_cache_donation
op.drop_index(op.f('ix_nationbuilder_cache_donation_version_transaction_id'), table_name='nationbuilder_cache_donation_version')
op.drop_index(op.f('ix_nationbuilder_cache_donation_version_operation_type'), table_name='nationbuilder_cache_donation_version')
op.drop_index(op.f('ix_nationbuilder_cache_donation_version_end_transaction_id'), table_name='nationbuilder_cache_donation_version')
op.drop_table('nationbuilder_cache_donation_version')
op.drop_table('nationbuilder_cache_donation')

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
DB schema for NationBuilder integration
"""
from .nationbuilder import NationBuilderCachePerson, NationBuilderCacheDonation

View file

@ -0,0 +1,116 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2024 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
NationBuilder cache tables
"""
import sqlalchemy as sa
from sqlalchemy import orm
from wuttjamaican.util import parse_list
from rattail.db import model
from rattail.db.util import normalize_full_name
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.Text(), 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
class NationBuilderCacheDonation(model.Base):
"""
Represents a Donation record in NationBuilder.
https://apiexplorer.nationbuilder.com/nationbuilder#Donations
"""
__tablename__ = 'nationbuilder_cache_donation'
__table_args__ = (
sa.UniqueConstraint('id', name='nationbuilder_cache_donation_uq_id'),
)
__versioned__ = {}
model_title = "NationBuilder Donation"
model_title_plural = "NationBuilder Donations"
uuid = model.uuid_column()
id = sa.Column(sa.Integer(), nullable=False)
author_id = sa.Column(sa.Integer(), nullable=True)
membership_id = sa.Column(sa.Integer(), nullable=True)
donor_id = sa.Column(sa.Integer(), nullable=True)
donor_external_id = sa.Column(sa.String(length=50), nullable=True)
email = sa.Column(sa.String(length=255), nullable=True)
amount = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True)
payment_type_name = sa.Column(sa.String(length=100), nullable=True)
check_number = sa.Column(sa.String(length=255), nullable=True)
tracking_code_slug = sa.Column(sa.String(length=255), nullable=True)
note = sa.Column(sa.Text(), nullable=True)
created_at = sa.Column(sa.DateTime(), nullable=True)
succeeded_at = sa.Column(sa.DateTime(), nullable=True)
failed_at = sa.Column(sa.DateTime(), nullable=True)
canceled_at = sa.Column(sa.DateTime(), nullable=True)
updated_at = sa.Column(sa.DateTime(), nullable=True)

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
rattail-nationbuilder importing
"""
from . import model

View file

@ -0,0 +1,39 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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
class NationBuilderCacheDonationImporter(ToRattail):
model_class = model.NationBuilderCacheDonation

View file

@ -0,0 +1,182 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
NationBuilder -> Rattail importing
"""
import datetime
import decimal
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
importers['NationBuilderCacheDonation'] = NationBuilderCacheDonationImporter
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)
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)
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=100,
progress=self.progress)
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)
else:
data['tags'] = None
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
class NationBuilderCacheDonationImporter(FromNationBuilder, nationbuilder_importing.model.NationBuilderCacheDonationImporter):
"""
Importer for NB Donation cache
"""
key = 'id'
supported_fields = [
'id',
'author_id',
'membership_id',
'donor_id',
'donor_external_id',
'email',
'amount',
'payment_type_name',
'check_number',
'tracking_code_slug',
'note',
'created_at',
'succeeded_at',
'failed_at',
'canceled_at',
'updated_at',
]
def get_host_objects(self):
return self.nationbuilder.get_donations(page_size=500)
def normalize_host_object(self, donation):
# nb. some fields may not be present in donation dict
data = dict([(field, donation.get(field))
for field in self.fields])
if data:
donor = donation.get('donor')
data['donor_external_id'] = donor.get('external_id') if donor else None
data['amount'] = decimal.Decimal('{:0.2f}'.format(donation['amount_in_cents'] / 100))
for field in ('created_at', 'succeeded_at', 'failed_at', 'canceled_at', 'updated_at'):
data[field] = self.normalize_timestamp(data[field])
return data

View file

@ -0,0 +1,49 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
rattail-nationbuilder version importing
"""
from rattail.importing import versions as base
class NationBuilderVersionMixin(object):
def add_nationbuilder_importers(self, importers):
importers['NationBuilderCachePerson'] = NationBuilderCachePersonImporter
importers['NationBuilderCacheDonation'] = NationBuilderCacheDonationImporter
return importers
class NationBuilderCachePersonImporter(base.VersionImporter):
@property
def host_model_class(self):
return self.model.NationBuilderCachePerson
class NationBuilderCacheDonationImporter(base.VersionImporter):
@property
def host_model_class(self):
return self.model.NationBuilderCacheDonation

View file

@ -0,0 +1,37 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
NationBuilder utils
"""
import warnings
def get_nationbuilder_url(config):
warnings.warn("get_nationbuilder_url() function is deprecated; "
"please use nationbuilder_handler.get_url() instead",
DeprecationWarning, stacklevel=2)
app = config.get_app()
nationbuilder = app.get_nationbuilder_handler()
return nationbuilder.get_url()

View file

@ -24,15 +24,23 @@
NationBuilder Web API
"""
import logging
import requests
log = logging.getLogger(__name__)
class NationBuilderWebAPI(object):
"""
Simple web API for NationBuilder.
https://nationbuilder.com/api_documentation
"""
def __init__(self, config, base_url=None, access_token=None, **kwargs):
def __init__(self, config, base_url=None, access_token=None,
max_retries=None, **kwargs):
self.config = config
self.app = self.config.get_app()
@ -43,6 +51,18 @@ class NationBuilderWebAPI(object):
self.access_token = access_token or self.config.require(
'nationbuilder', 'api.access_token')
if max_retries is not None:
self.max_retries = max_retries
else:
self.max_retries = self.config.getint('nationbuilder',
'api.max_retries')
self.session = requests.Session()
if self.max_retries is not None:
adapter = requests.adapters.HTTPAdapter(max_retries=self.max_retries)
self.session.mount(self.base_url, adapter)
def _request(self, request_method, api_method, params=None):
"""
Perform a request for the given API method, and return the response.
@ -53,8 +73,8 @@ class NationBuilderWebAPI(object):
params['access_token'] = self.access_token
if request_method == 'GET':
response = requests.get('{}/{}'.format(self.base_url, api_method),
params=params)
response = self.session.get('{}/{}'.format(self.base_url, api_method),
params=params)
else:
raise NotImplementedError("unknown request method: {}".format(
@ -69,12 +89,16 @@ class NationBuilderWebAPI(object):
"""
return self._request('GET', api_method, params=params)
def get_people(self, page_size=100, progress=None, **kwargs):
def get_people(self, page_size=10, max_pages=None, progress=None, **kwargs):
"""
Retrieve all Person records.
https://apiexplorer.nationbuilder.com/nationbuilder#People
"""
# nb. found this limit in practice, but it is not documented?!
if page_size > 100:
raise ValueError("page_size cannot be more than 100")
response = self.get('/api/v1/people/count')
count = response.json()['people_count']
pages = count // page_size
@ -89,12 +113,16 @@ 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
def get_people_with_tag(self, tag, page_size=100, **kwargs):
def get_people_with_tag(self, tag, page_size=10, max_pages=None, **kwargs):
"""
Retrieve all Person records with the given tag.
@ -108,11 +136,74 @@ class NationBuilderWebAPI(object):
response = self.get(api_method)
data = response.json()
people.extend(data['results'])
pages = 1
# get more pages, until complete
while data['next']:
if max_pages and pages >= max_pages:
break
response = self.get(data['next'])
data = response.json()
people.extend(data['results'])
pages += 1
return people
def get_donations(self, page_size=10, max_pages=None, **kwargs):
"""
Retrieve all Donation records.
https://apiexplorer.nationbuilder.com/nationbuilder#Donations
"""
donations = []
# get first page
url = f'/api/v1/donations?limit={page_size}'
response = self.get(url)
data = response.json()
donations.extend(data['results'])
pages = 1
# get more pages, until complete
while data['next']:
if max_pages and pages >= max_pages:
break
response = self.get(data['next'])
data = response.json()
donations.extend(data['results'])
pages += 1
log.debug("have fetched %s pages", pages)
return donations
def search_donations(self, page_size=10, max_pages=None, **kwargs):
"""
Search for matching Donation records.
https://apiexplorer.nationbuilder.com/nationbuilder#Donations
"""
donations = []
# get first page
url = f'/api/v1/donations/search?limit={page_size}'
for field in ('created_since', 'succeeded_since', 'failed_since'):
value = kwargs.get(field)
if value:
value = value.strftime('%Y-%m-%dT%H:%M:%S%z')
url += f"&{field}={value}"
response = self.get(url)
data = response.json()
donations.extend(data['results'])
pages = 1
# get more pages, until complete
while data['next']:
if max_pages and pages >= max_pages:
break
response = self.get(data['next'])
data = response.json()
donations.extend(data['results'])
pages += 1
log.debug("have fetched %s pages", pages)
return donations

View file

@ -1,71 +0,0 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
rattail-nationbuilder setup script
"""
import os
from setuptools import setup, find_packages
here = os.path.abspath(os.path.dirname(__file__))
exec(open(os.path.join(here, 'rattail_nationbuilder', '_version.py')).read())
README = open(os.path.join(here, 'README.md')).read()
setup(
name = "rattail-nationbuilder",
version = __version__,
author = "Lance Edgar",
author_email = "lance@edbob.org",
url = "https://rattailproject.org/",
license = "GNU GPL v3",
description = "Rattail integration package for NationBuilder",
long_description = README,
classifiers = [
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Topic :: Office/Business',
'Topic :: Software Development :: Libraries :: Python Modules',
],
install_requires = [
'rattail',
# TODO: these may be needed to build/release package
#'build',
#'invoke',
#'twine',
],
packages = find_packages(),
include_package_data = True,
entry_points = {
},
)

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2023 Lance Edgar
# Copyright © 2010-2024 Lance Edgar
#
# This file is part of Rattail.
#
@ -30,25 +30,17 @@ import shutil
from invoke import task
here = os.path.abspath(os.path.dirname(__file__))
exec(open(os.path.join(here, 'rattail_nationbuilder', '_version.py')).read())
@task
def release(c):
"""
Release a new version of rattail-nationbuilder
"""
# rebuild local tar.gz file for distribution
# rebuild package
if os.path.exists('dist'):
shutil.rmtree('dist')
if os.path.exists('rattail_nationbuilder.egg-info'):
shutil.rmtree('rattail_nationbuilder.egg-info')
c.run('python -m build --sdist')
# filename of built package
filename = 'rattail-nationbuilder-{}.tar.gz'.format(__version__)
# TODO: uncomment and update these details, to upload to private PyPI
#c.run('scp dist/{} rattail@pypi.example.com:/srv/pypi/rattail-nationbuilder/'.format(filename))
# TODO: or, uncomment this to upload to *public* PyPI
#c.run('twine upload dist/{}'.format(filename))
# upload to PyPI
c.run('twine upload dist/*')