From 259d3b0f33c48450ec27b03da865e864bc85d28e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 29 Jan 2022 19:33:03 -0600 Subject: [PATCH 01/42] Allow import of harvest time entries w/ missing project --- rattail_harvest/importing/harvest.py | 26 ++++++-------- rattail_harvest/importing/model.py | 51 ++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/rattail_harvest/importing/harvest.py b/rattail_harvest/importing/harvest.py index 8cbd9dd..c686e84 100644 --- a/rattail_harvest/importing/harvest.py +++ b/rattail_harvest/importing/harvest.py @@ -183,14 +183,6 @@ class HarvestTimeEntryImporter(FromHarvest, rattail_harvest_importing.model.Harv Import time entry data from Harvest """ - def setup(self): - super(HarvestTimeEntryImporter, self).setup() - model = self.model - - self.harvest_projects_by_id = self.app.cache_model(self.session, - model.HarvestProject, - key='id') - def get_host_objects(self): return self.webapi.get_time_entries(**{'from': self.start_date, 'to': self.end_date}) @@ -202,16 +194,20 @@ class HarvestTimeEntryImporter(FromHarvest, rattail_harvest_importing.model.Harv data['user_id'] = entry['user']['id'] data['client_id'] = entry['client']['id'] - - data['project_id'] = entry['project']['id'] - if data['project_id'] not in self.harvest_projects_by_id: - log.warning("time entry references non-existent project id %s: %s", - data['project_id'], entry) - data['project_id'] = None - data['task_id'] = entry['task']['id'] data['invoice_id'] = entry['invoice']['id'] if entry['invoice'] else None + # project_id + if 'project_id' in self.fields: + data['project_id'] = entry['project']['id'] + project = self.get_harvest_project(data['project_id']) + if not project: + logger = log.warning if self.warn_for_unknown_project else log.debug + logger("time entry references non-existent project id %s: %s", + data['project_id'], entry) + if not self.auto_create_unknown_project: + data['project_id'] = None + # spent_date spent_date = data['spent_date'] if spent_date: diff --git a/rattail_harvest/importing/model.py b/rattail_harvest/importing/model.py index 6fbcfdb..086965c 100644 --- a/rattail_harvest/importing/model.py +++ b/rattail_harvest/importing/model.py @@ -24,10 +24,15 @@ rattail-harvest model importers """ +import logging + from rattail.importing.model import ToRattail from rattail_harvest.db import model +log = logging.getLogger(__name__) + + ############################## # harvest cache models ############################## @@ -47,7 +52,53 @@ class HarvestTaskImporter(ToRattail): class HarvestTimeEntryImporter(ToRattail): model_class = model.HarvestTimeEntry + # flags to auto-create records for "unknown" references + auto_create_unknown_project = True + + # flags to log warning vs. debug for "unknown" references + warn_for_unknown_project = True + + def setup(self): + super(HarvestTimeEntryImporter, self).setup() + model = self.model + + if 'project_id' in self.fields: + self.harvest_projects_by_id = self.app.cache_model( + self.session, model.HarvestProject, key='id') + def cache_query(self): query = super(HarvestTimeEntryImporter, self).cache_query() return query.filter(self.model_class.spent_date >= self.start_date)\ .filter(self.model_class.spent_date <= self.end_date) + + def get_harvest_project(self, project_id): + if hasattr(self, 'harvest_projects_by_id'): + return self.harvest_projects_by_id.get(project_id) + + model = self.model + return self.session.query(model.HarvestProject)\ + .filter(model.HarvestProject.id == project_id)\ + .first() + + def update_object(self, entry, data, local_data=None): + entry = super(HarvestTimeEntryImporter, self).update_object(entry, data, local_data) + model = self.model + + if 'project_id' in self.fields: + project_id = data['project_id'] + project = self.get_harvest_project(project_id) + if not project: + logger = log.warning if self.warn_for_unknown_project else log.debug + logger("unknown project id %s for time entry id %s: %s", + project_id, entry.id, entry) + if self.auto_create_unknown_project: + project = model.HarvestProject() + project.id = project_id + project.name = "(unknown)" + self.session.add(project) + if hasattr(self, 'harvest_projects_by_id'): + self.harvest_projects_by_id[project_id] = project + elif entry.project_id: + entry.project_id = None + + return entry From 3883a8551f29f41ed44a9e1f876a0f895e8863f8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 30 Jan 2022 12:14:42 -0600 Subject: [PATCH 02/42] Add `HarvestProject.deleted` flag to track deletions in Harvest set this flag instead of deleting project, so we do not lose other info about it. can delete manually if truly unwanted --- .../5505c0e60d28_add_project_deleted.py | 33 +++++++++++++++++++ rattail_harvest/db/model/harvest.py | 4 +++ rattail_harvest/importing/harvest.py | 25 ++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 rattail_harvest/db/alembic/versions/5505c0e60d28_add_project_deleted.py diff --git a/rattail_harvest/db/alembic/versions/5505c0e60d28_add_project_deleted.py b/rattail_harvest/db/alembic/versions/5505c0e60d28_add_project_deleted.py new file mode 100644 index 0000000..042663d --- /dev/null +++ b/rattail_harvest/db/alembic/versions/5505c0e60d28_add_project_deleted.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8; -*- +"""add project.deleted + +Revision ID: 5505c0e60d28 +Revises: d59ce24c2f9f +Create Date: 2022-01-30 12:08:04.338229 + +""" + +# revision identifiers, used by Alembic. +revision = '5505c0e60d28' +down_revision = 'd59ce24c2f9f' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa +import rattail.db.types + + + +def upgrade(): + + # harvest_project + op.add_column('harvest_project', sa.Column('deleted', sa.Boolean(), nullable=True)) + op.add_column('harvest_project_version', sa.Column('deleted', sa.Boolean(), autoincrement=False, nullable=True)) + + +def downgrade(): + + # harvest_project + op.drop_column('harvest_project_version', 'deleted') + op.drop_column('harvest_project', 'deleted') diff --git a/rattail_harvest/db/model/harvest.py b/rattail_harvest/db/model/harvest.py index d7a098c..4d0e80e 100644 --- a/rattail_harvest/db/model/harvest.py +++ b/rattail_harvest/db/model/harvest.py @@ -190,6 +190,10 @@ class HarvestProject(model.Base): updated_at = sa.Column(sa.DateTime(), nullable=True) + deleted = sa.Column(sa.Boolean(), nullable=True, doc=""" + Flag indicating the record has been deleted in Harvest. + """) + def __str__(self): return self.name or '' diff --git a/rattail_harvest/importing/harvest.py b/rattail_harvest/importing/harvest.py index c686e84..cde1e1c 100644 --- a/rattail_harvest/importing/harvest.py +++ b/rattail_harvest/importing/harvest.py @@ -28,6 +28,8 @@ import datetime import decimal import logging +import sqlalchemy as sa + from rattail import importing from rattail.util import OrderedDict from rattail_harvest import importing as rattail_harvest_importing @@ -126,6 +128,22 @@ class HarvestProjectImporter(FromHarvest, rattail_harvest_importing.model.Harves Import project data from Harvest """ + @property + def supported_fields(self): + fields = list(super(HarvestProjectImporter, self).supported_fields) + + # this is for local tracking only; is not in harvest + fields.remove('deleted') + + return fields + + def cache_query(self): + model = self.model + return self.session.query(model.HarvestProject)\ + .filter(sa.or_( + model.HarvestProject.deleted == False, + model.HarvestProject.deleted == None)) + def get_host_objects(self): return self.webapi.get_projects()['projects'] @@ -168,6 +186,13 @@ class HarvestProjectImporter(FromHarvest, rattail_harvest_importing.model.Harves return data + def can_delete_object(self, project, data): + return not project.deleted + + def delete_object(self, project): + project.deleted = True + return True + class HarvestTaskImporter(FromHarvest, rattail_harvest_importing.model.HarvestTaskImporter): """ From ec78f8c9c45ef8324e25b728288d247809a93c5b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 30 Jan 2022 17:40:19 -0600 Subject: [PATCH 03/42] Add `HarvestUser.person` association importer does not set this; you must do so manually --- .../6bc1cb21d920_add_harvest_user_person.py | 35 +++++ rattail_harvest/db/model/harvest.py | 14 ++ rattail_harvest/harvest/importing/__init__.py | 27 ++++ rattail_harvest/harvest/importing/model.py | 120 ++++++++++++++++++ rattail_harvest/harvest/webapi.py | 9 ++ rattail_harvest/importing/harvest.py | 13 +- 6 files changed, 210 insertions(+), 8 deletions(-) create mode 100644 rattail_harvest/db/alembic/versions/6bc1cb21d920_add_harvest_user_person.py create mode 100644 rattail_harvest/harvest/importing/__init__.py create mode 100644 rattail_harvest/harvest/importing/model.py diff --git a/rattail_harvest/db/alembic/versions/6bc1cb21d920_add_harvest_user_person.py b/rattail_harvest/db/alembic/versions/6bc1cb21d920_add_harvest_user_person.py new file mode 100644 index 0000000..31cca85 --- /dev/null +++ b/rattail_harvest/db/alembic/versions/6bc1cb21d920_add_harvest_user_person.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8; -*- +"""add harvest_user.person + +Revision ID: 6bc1cb21d920 +Revises: 5505c0e60d28 +Create Date: 2022-01-30 16:49:32.271745 + +""" + +# revision identifiers, used by Alembic. +revision = '6bc1cb21d920' +down_revision = '5505c0e60d28' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa +import rattail.db.types + + + +def upgrade(): + + # harvest_user + op.add_column('harvest_user', sa.Column('person_uuid', sa.String(length=32), nullable=True)) + op.create_foreign_key('harvest_user_fk_person', 'harvest_user', 'person', ['person_uuid'], ['uuid']) + op.add_column('harvest_user_version', sa.Column('person_uuid', sa.String(length=32), autoincrement=False, nullable=True)) + + +def downgrade(): + + # harvest_user + op.drop_column('harvest_user_version', 'person_uuid') + op.drop_constraint('harvest_user_fk_person', 'harvest_user', type_='foreignkey') + op.drop_column('harvest_user', 'person_uuid') diff --git a/rattail_harvest/db/model/harvest.py b/rattail_harvest/db/model/harvest.py index 4d0e80e..7ac7506 100644 --- a/rattail_harvest/db/model/harvest.py +++ b/rattail_harvest/db/model/harvest.py @@ -39,6 +39,7 @@ class HarvestUser(model.Base): """ __tablename__ = 'harvest_user' __table_args__ = ( + sa.ForeignKeyConstraint(['person_uuid'], ['person.uuid'], name='harvest_user_fk_person'), sa.UniqueConstraint('id', name='harvest_user_uq_id'), ) __versioned__ = {} @@ -90,6 +91,19 @@ class HarvestUser(model.Base): updated_at = sa.Column(sa.DateTime(), nullable=True) + person_uuid = sa.Column(sa.String(length=32), nullable=True) + person = orm.relationship( + model.Person, + doc=""" + Reference to the person associated with this Harvest user. + """, + backref=orm.backref( + 'harvest_users', + doc=""" + List of all Harvest user accounts for the person. + """) + ) + def __str__(self): return normalize_full_name(self.first_name, self.last_name) diff --git a/rattail_harvest/harvest/importing/__init__.py b/rattail_harvest/harvest/importing/__init__.py new file mode 100644 index 0000000..e0b3e09 --- /dev/null +++ b/rattail_harvest/harvest/importing/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 . +# +################################################################################ +""" +Exporting data to Harvest +""" + +from . import model diff --git a/rattail_harvest/harvest/importing/model.py b/rattail_harvest/harvest/importing/model.py new file mode 100644 index 0000000..47e85de --- /dev/null +++ b/rattail_harvest/harvest/importing/model.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 . +# +################################################################################ +""" +Harvest model importers +""" + +from rattail import importing +from rattail_harvest.harvest.webapi import make_harvest_webapi + + +class ToHarvest(importing.Importer): + + def setup(self): + super(ToHarvest, self).setup() + self.webapi = make_harvest_webapi(self.config) + + +class TimeEntryImporter(ToHarvest): + """ + Harvest time entry data importer. + """ + model_name = 'TimeEntry' + supported_fields = [ + 'user_id', + 'project_id', + 'task_id', + 'spent_date', + # 'started_time', + # 'ended_time', + 'hours', + 'notes', + ] + caches_local_data = True + + def cache_local_data(self, host_data=None): + """ + Fetch existing time entries from Harvest. + """ + cache = {} + entries = self.webapi.get_time_entries(**{'from': self.start_date, + 'to': self.end_date}) + for entry in entries: + data = self.normalize_local_object(entry) + if data: + normal = self.normalize_cache_object(entry, data) + key = self.get_cache_key(entry, normal) + cache[key] = normal + return cache + + def normalize_local_object(self, entry): + data = { + 'project_id': entry['project']['id'], + 'task_id': entry['task']['id'], + 'spent_date': entry['spent_date'], + # 'started_time': entry['started_time'], + # 'ended_time': entry['ended_time'], + 'hours': entry['hours'], + 'notes': entry['notes'], + } + + if 'user_id' in self.fields: + data['user_id'] = entry['user']['id'] + + return data + + def create_object(self, key, host_data): + if self.dry_run: + # mock out return value + result = dict(host_data) + if 'user_id' in self.fields: + result['user'] = {'id': result['user_id']} + result['project'] = {'id': result['project_id']} + result['task'] = {'id': result['task_id']} + return result + + kwargs = { + 'project_id': host_data['project_id'], + 'task_id': host_data['task_id'], + 'spent_date': host_data['spent_date'], + # 'started_time': host_data['started_time'], + # 'ended_time': host_data['ended_time'], + 'hours': host_data['hours'], + 'notes': host_data['notes'], + } + if 'user_id' in self.fields: + kwargs['user_id'] = host_data['user_id'] + entry = self.webapi.put_time_entry(**kwargs) + return entry + + def update_object(self, obj, host_data, local_data=None, all_fields=False): + if self.dry_run: + return obj + + raise NotImplementedError + + def delete_object(self, obj): + if self.dry_run: + return True + + raise NotImplementedError diff --git a/rattail_harvest/harvest/webapi.py b/rattail_harvest/harvest/webapi.py index 6107b19..a037f1b 100644 --- a/rattail_harvest/harvest/webapi.py +++ b/rattail_harvest/harvest/webapi.py @@ -167,3 +167,12 @@ class HarvestWebAPI(object): raise ValueError("must provide all of: {}".format(', '.join(required))) response = self.post('/time_entries', params=kwargs) return response.json() + + +def make_harvest_webapi(config): + access_token = config.require('harvest', 'api.access_token') + account_id = config.require('harvest', 'api.account_id') + user_agent = config.require('harvest', 'api.user_agent') + return HarvestWebAPI(access_token=access_token, + account_id=account_id, + user_agent=user_agent) diff --git a/rattail_harvest/importing/harvest.py b/rattail_harvest/importing/harvest.py index cde1e1c..695d04d 100644 --- a/rattail_harvest/importing/harvest.py +++ b/rattail_harvest/importing/harvest.py @@ -33,7 +33,7 @@ import sqlalchemy as sa from rattail import importing from rattail.util import OrderedDict from rattail_harvest import importing as rattail_harvest_importing -from rattail_harvest.harvest.webapi import HarvestWebAPI +from rattail_harvest.harvest.webapi import make_harvest_webapi log = logging.getLogger(__name__) @@ -71,13 +71,7 @@ class FromHarvest(importing.Importer): def setup(self): super(FromHarvest, self).setup() - - access_token = self.config.require('harvest', 'api.access_token') - account_id = self.config.require('harvest', 'api.account_id') - user_agent = self.config.require('harvest', 'api.user_agent') - self.webapi = HarvestWebAPI(access_token=access_token, - account_id=account_id, - user_agent=user_agent) + self.webapi = make_harvest_webapi(self.config) def time_from_harvest(self, value): # all harvest times appear to come as UTC, so no conversion needed @@ -105,6 +99,9 @@ class HarvestUserImporter(FromHarvest, rattail_harvest_importing.model.HarvestUs def supported_fields(self): fields = list(super(HarvestUserImporter, self).supported_fields) + # this is for local tracking only; is not in harvest + fields.remove('person_uuid') + # this used to be in harvest i thought, but is no longer? fields.remove('name') From 1f54ddc9e4aa7584eae99a2ef3a5b5c934ac040c Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 30 Jan 2022 19:34:15 -0600 Subject: [PATCH 04/42] Add 'id' key field for exporting TimeEntry to Harvest --- rattail_harvest/harvest/importing/model.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rattail_harvest/harvest/importing/model.py b/rattail_harvest/harvest/importing/model.py index 47e85de..582c159 100644 --- a/rattail_harvest/harvest/importing/model.py +++ b/rattail_harvest/harvest/importing/model.py @@ -40,7 +40,9 @@ class TimeEntryImporter(ToHarvest): Harvest time entry data importer. """ model_name = 'TimeEntry' + key = 'id' supported_fields = [ + 'id', 'user_id', 'project_id', 'task_id', @@ -69,6 +71,7 @@ class TimeEntryImporter(ToHarvest): def normalize_local_object(self, entry): data = { + 'id': entry['id'], 'project_id': entry['project']['id'], 'task_id': entry['task']['id'], 'spent_date': entry['spent_date'], @@ -83,6 +86,14 @@ class TimeEntryImporter(ToHarvest): return data + def get_next_harvest_id(self): + if hasattr(self, 'next_harvest_id'): + next_id = self.next_harvest_id + else: + next_id = 1 + self.next_harvest_id = next_id + 1 + return -next_id + def create_object(self, key, host_data): if self.dry_run: # mock out return value From 14f9dfdaa9455fe85f1b27f4107a5dcdacd4d27b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 30 Jan 2022 20:28:24 -0600 Subject: [PATCH 05/42] Add `client_id` field for TimeEntry export to Harvest --- rattail_harvest/harvest/importing/model.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rattail_harvest/harvest/importing/model.py b/rattail_harvest/harvest/importing/model.py index 582c159..8b55b15 100644 --- a/rattail_harvest/harvest/importing/model.py +++ b/rattail_harvest/harvest/importing/model.py @@ -44,6 +44,7 @@ class TimeEntryImporter(ToHarvest): supported_fields = [ 'id', 'user_id', + 'client_id', 'project_id', 'task_id', 'spent_date', @@ -72,6 +73,7 @@ class TimeEntryImporter(ToHarvest): def normalize_local_object(self, entry): data = { 'id': entry['id'], + 'client_id': entry['client']['id'], 'project_id': entry['project']['id'], 'task_id': entry['task']['id'], 'spent_date': entry['spent_date'], @@ -100,11 +102,14 @@ class TimeEntryImporter(ToHarvest): result = dict(host_data) if 'user_id' in self.fields: result['user'] = {'id': result['user_id']} + if 'client_id' in self.fields: + result['client'] = {'id': result['client_id']} result['project'] = {'id': result['project_id']} result['task'] = {'id': result['task_id']} return result kwargs = { + 'client_id': host_data['client_id'], 'project_id': host_data['project_id'], 'task_id': host_data['task_id'], 'spent_date': host_data['spent_date'], From 63d238d307c8ec7d84d1498e7f298d8578600dc0 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 31 Jan 2022 10:37:33 -0600 Subject: [PATCH 06/42] Coalesce HarvestUser telephone to null --- rattail_harvest/importing/harvest.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rattail_harvest/importing/harvest.py b/rattail_harvest/importing/harvest.py index 695d04d..3d5abfd 100644 --- a/rattail_harvest/importing/harvest.py +++ b/rattail_harvest/importing/harvest.py @@ -110,6 +110,15 @@ class HarvestUserImporter(FromHarvest, rattail_harvest_importing.model.HarvestUs def get_host_objects(self): return self.webapi.get_users()['users'] + def normalize_host_object(self, user): + data = super(HarvestUserImporter, self).normalize_host_object(user) + if data: + + if data['telephone'] == '': + data['telephone'] = None + + return data + class HarvestClientImporter(FromHarvest, rattail_harvest_importing.model.HarvestClientImporter): """ From caa1ef93b7595699fd6044aab057b4df0f0499ca Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 9 Feb 2022 16:11:32 -0600 Subject: [PATCH 07/42] Add version importers for Harvest cache models --- rattail_harvest/importing/versions.py | 73 +++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 rattail_harvest/importing/versions.py diff --git a/rattail_harvest/importing/versions.py b/rattail_harvest/importing/versions.py new file mode 100644 index 0000000..3dd5571 --- /dev/null +++ b/rattail_harvest/importing/versions.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 -> Rattail "versions" data import +""" + +from rattail.importing import versions as base + + +class HarvestVersionMixin(object): + + def add_harvest_importers(self, importers): + importers['HarvestUser'] = HarvestUserImporter + importers['HarvestClient'] = HarvestClientImporter + importers['HarvestProject'] = HarvestProjectImporter + importers['HarvestTask'] = HarvestTaskImporter + importers['HarvestTimeEntry'] = HarvestTimeEntryImporter + return importers + + +class HarvestUserImporter(base.VersionImporter): + + @property + def host_model_class(self): + return self.model.HarvestUser + + +class HarvestClientImporter(base.VersionImporter): + + @property + def host_model_class(self): + return self.model.HarvestClient + + +class HarvestProjectImporter(base.VersionImporter): + + @property + def host_model_class(self): + return self.model.HarvestProject + + +class HarvestTaskImporter(base.VersionImporter): + + @property + def host_model_class(self): + return self.model.HarvestTask + + +class HarvestTimeEntryImporter(base.VersionImporter): + + @property + def host_model_class(self): + return self.model.HarvestTimeEntry From ac653d25f1ea9449397457ed9969501eb6730157 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 11 Feb 2022 19:16:03 -0600 Subject: [PATCH 08/42] Add convenience function to get configured Harvest URL --- rattail_harvest/harvest/config.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 rattail_harvest/harvest/config.py diff --git a/rattail_harvest/harvest/config.py b/rattail_harvest/harvest/config.py new file mode 100644 index 0000000..3a2cbd6 --- /dev/null +++ b/rattail_harvest/harvest/config.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 . +# +################################################################################ +""" +Harvest config +""" + + +def get_harvest_url(config): + url = config.get('harvest', 'url') + if url: + return url.rstrip('/') From e79686b5b79d3ba3abb197726a9025931d35ba9d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 17 Feb 2022 06:57:36 -0600 Subject: [PATCH 09/42] Avoid importing Harvest times for which timer is still running --- rattail_harvest/importing/harvest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rattail_harvest/importing/harvest.py b/rattail_harvest/importing/harvest.py index 3d5abfd..fa80b0b 100644 --- a/rattail_harvest/importing/harvest.py +++ b/rattail_harvest/importing/harvest.py @@ -223,6 +223,10 @@ class HarvestTimeEntryImporter(FromHarvest, rattail_harvest_importing.model.Harv if not data: return + if entry['is_running']: + log.warning("Harvest time entry is still running: %s", entry) + return + data['user_id'] = entry['user']['id'] data['client_id'] = entry['client']['id'] data['task_id'] = entry['task']['id'] From d8e9714771d1831dacee1b5d3de22509b2380f1f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 17 Feb 2022 07:39:29 -0600 Subject: [PATCH 10/42] Avoid entries w/ timer still running when exporting to Harvest i.e. ignore entries on Harvest, which have timer running --- rattail_harvest/harvest/importing/model.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/rattail_harvest/harvest/importing/model.py b/rattail_harvest/harvest/importing/model.py index 8b55b15..21e070d 100644 --- a/rattail_harvest/harvest/importing/model.py +++ b/rattail_harvest/harvest/importing/model.py @@ -60,14 +60,19 @@ class TimeEntryImporter(ToHarvest): Fetch existing time entries from Harvest. """ cache = {} + # TODO: we try to avoid entries w/ timer still running here, + # but for some reason they still come back, so double-check entries = self.webapi.get_time_entries(**{'from': self.start_date, - 'to': self.end_date}) + 'to': self.end_date, + 'is_running': False}) for entry in entries: - data = self.normalize_local_object(entry) - if data: - normal = self.normalize_cache_object(entry, data) - key = self.get_cache_key(entry, normal) - cache[key] = normal + # double-check here + if not entry['is_running']: + data = self.normalize_local_object(entry) + if data: + normal = self.normalize_cache_object(entry, data) + key = self.get_cache_key(entry, normal) + cache[key] = normal return cache def normalize_local_object(self, entry): From c6332be4530ee7eacaee4f320cddda3dd1fec929 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 20 Feb 2022 21:15:48 -0600 Subject: [PATCH 11/42] Add `stop_time_entry()` API method --- rattail_harvest/harvest/webapi.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/rattail_harvest/harvest/webapi.py b/rattail_harvest/harvest/webapi.py index a037f1b..76036a6 100644 --- a/rattail_harvest/harvest/webapi.py +++ b/rattail_harvest/harvest/webapi.py @@ -58,6 +58,9 @@ class HarvestWebAPI(object): elif request_method == 'POST': response = requests.post('{}/{}'.format(self.base_url, api_method), headers=headers, params=params) + elif request_method == 'PATCH': + response = requests.patch('{}/{}'.format(self.base_url, api_method), + headers=headers, params=params) else: raise NotImplementedError("unknown request method: {}".format( request_method)) @@ -76,6 +79,12 @@ class HarvestWebAPI(object): """ return self._request('POST', api_method, params=params) + def patch(self, api_method, params=None): + """ + Perform a PATCH request for the given API method, and return the response. + """ + return self._request('PATCH', api_method, params=params) + def get_company(self): """ Retrieves the company for the currently authenticated user. @@ -168,6 +177,15 @@ class HarvestWebAPI(object): response = self.post('/time_entries', params=kwargs) return response.json() + def stop_time_entry(self, time_entry_id): + """ + Stop a running time entry. + + https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/#stop-a-running-time-entry + """ + response = self.patch('/time_entries/{}/stop'.format(time_entry_id)) + return response.json() + def make_harvest_webapi(config): access_token = config.require('harvest', 'api.access_token') From 0ee4d521454d458f16988e221b8c93967d82fe1a Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 22 Feb 2022 12:02:58 -0600 Subject: [PATCH 12/42] Lower log level when harvest time entry still running during import that is not exactly an unexpected situation, no need to raise awareness to it probably --- rattail_harvest/importing/harvest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rattail_harvest/importing/harvest.py b/rattail_harvest/importing/harvest.py index fa80b0b..a64c88d 100644 --- a/rattail_harvest/importing/harvest.py +++ b/rattail_harvest/importing/harvest.py @@ -224,7 +224,7 @@ class HarvestTimeEntryImporter(FromHarvest, rattail_harvest_importing.model.Harv return if entry['is_running']: - log.warning("Harvest time entry is still running: %s", entry) + log.debug("Harvest time entry is still running: %s", entry) return data['user_id'] = entry['user']['id'] From d84cc7a9d94e0746a815c46d6d01f2391643e97d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 28 Feb 2022 22:05:34 -0600 Subject: [PATCH 13/42] Add config extension to define importers --- rattail_harvest/config.py | 42 +++++++++++++++++++++++++++++++++++++++ setup.py | 4 ++++ 2 files changed, 46 insertions(+) create mode 100644 rattail_harvest/config.py diff --git a/rattail_harvest/config.py b/rattail_harvest/config.py new file mode 100644 index 0000000..9fedb3e --- /dev/null +++ b/rattail_harvest/config.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 RattailHarvestExtension(ConfigExtension): + """ + Config extension for rattail-harvest. + """ + key = 'rattail_harvest' + + def configure(self, config): + + # rattail import-harvest + config.setdefault('rattail.importing', 'to_rattail.from_harvest.import.default_handler', + 'rattail_harvest.importing.harvest:FromHarvestToRattail') + config.setdefault('rattail.importing', 'to_rattail.from_harvest.import.default_cmd', + 'rattail import-harvest') diff --git a/setup.py b/setup.py index c11e7a1..0d5a699 100644 --- a/setup.py +++ b/setup.py @@ -99,6 +99,10 @@ setup( 'import-harvest = rattail_harvest.commands:ImportHarvest', ], + 'rattail.config.extensions': [ + 'rattail_harvest = rattail_harvest.config:RattailHarvestExtension', + ], + 'rattail.importing': [ 'to_rattail.from_harvest.import = rattail_harvest.importing.harvest:FromHarvestToRattail', ], From 03066f1135b13204b6241235c0ee0ec5a1c1f4e8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 6 Mar 2022 11:09:59 -0600 Subject: [PATCH 14/42] Add basic support for updating a Harvest Time Entry via API --- rattail_harvest/harvest/importing/model.py | 18 ++++++++++++++---- rattail_harvest/harvest/webapi.py | 14 +++++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/rattail_harvest/harvest/importing/model.py b/rattail_harvest/harvest/importing/model.py index 21e070d..d445def 100644 --- a/rattail_harvest/harvest/importing/model.py +++ b/rattail_harvest/harvest/importing/model.py @@ -128,13 +128,23 @@ class TimeEntryImporter(ToHarvest): entry = self.webapi.put_time_entry(**kwargs) return entry - def update_object(self, obj, host_data, local_data=None, all_fields=False): + def update_object(self, entry, host_data, local_data=None, all_fields=False): if self.dry_run: - return obj + return entry - raise NotImplementedError + kwargs = { + 'project_id': host_data['project_id'], + 'task_id': host_data['task_id'], + 'spent_date': host_data['spent_date'], + # 'started_time': host_data['started_time'], + # 'ended_time': host_data['ended_time'], + 'hours': host_data['hours'], + 'notes': host_data['notes'], + } - def delete_object(self, obj): + return self.webapi.update_time_entry(entry['id'], **kwargs) + + def delete_object(self, entry): if self.dry_run: return True diff --git a/rattail_harvest/harvest/webapi.py b/rattail_harvest/harvest/webapi.py index 76036a6..14c1727 100644 --- a/rattail_harvest/harvest/webapi.py +++ b/rattail_harvest/harvest/webapi.py @@ -163,7 +163,7 @@ class HarvestWebAPI(object): response = self.get('/time_entries/{}'.format(time_entry_id)) return response.json() - def put_time_entry(self, **kwargs): + def create_time_entry(self, **kwargs): """ Create a new time entry. All kwargs are passed on as POST parameters. @@ -177,6 +177,9 @@ class HarvestWebAPI(object): response = self.post('/time_entries', params=kwargs) return response.json() + # TODO: deprecate / remove this + put_time_entry = create_time_entry + def stop_time_entry(self, time_entry_id): """ Stop a running time entry. @@ -186,6 +189,15 @@ class HarvestWebAPI(object): response = self.patch('/time_entries/{}/stop'.format(time_entry_id)) return response.json() + def update_time_entry(self, time_entry_id, **kwargs): + """ + Update a time entry. + + https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/#update-a-time-entry + """ + response = self.patch('/time_entries/{}'.format(time_entry_id), params=kwargs) + return response.json() + def make_harvest_webapi(config): access_token = config.require('harvest', 'api.access_token') From 782cb1fcec243760ebe2ce0b144ada9eac7223e6 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 1 Jul 2022 12:46:06 -0500 Subject: [PATCH 15/42] Fix API call to return all Harvest Projects --- rattail_harvest/harvest/webapi.py | 12 +++++++++++- rattail_harvest/importing/harvest.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/rattail_harvest/harvest/webapi.py b/rattail_harvest/harvest/webapi.py index 14c1727..03e57bd 100644 --- a/rattail_harvest/harvest/webapi.py +++ b/rattail_harvest/harvest/webapi.py @@ -122,7 +122,17 @@ class HarvestWebAPI(object): https://help.getharvest.com/api-v2/projects-api/projects/projects/#list-all-projects """ response = self.get('/projects', params=kwargs) - return response.json() + data = response.json() + projects = data['projects'] + while data['next_page']: + + kw = dict(kwargs) + kw['page'] = data['next_page'] + response = self.get('/projects', params=kw) + data = response.json() + projects.extend(data['projects']) + + return projects def get_tasks(self, **kwargs): """ diff --git a/rattail_harvest/importing/harvest.py b/rattail_harvest/importing/harvest.py index a64c88d..6fe466b 100644 --- a/rattail_harvest/importing/harvest.py +++ b/rattail_harvest/importing/harvest.py @@ -151,7 +151,7 @@ class HarvestProjectImporter(FromHarvest, rattail_harvest_importing.model.Harves model.HarvestProject.deleted == None)) def get_host_objects(self): - return self.webapi.get_projects()['projects'] + return self.webapi.get_projects() def normalize_host_object(self, project): data = super(HarvestProjectImporter, self).normalize_host_object(project) From e239ea70e4775967c4434ec47be437187b1225d8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 14 Nov 2022 17:43:26 -0600 Subject: [PATCH 16/42] Stop importing certain fields for HarvestUser cache table since the Harvest API is suddenly no longer providing the values --- rattail_harvest/importing/harvest.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rattail_harvest/importing/harvest.py b/rattail_harvest/importing/harvest.py index 6fe466b..6153348 100644 --- a/rattail_harvest/importing/harvest.py +++ b/rattail_harvest/importing/harvest.py @@ -114,6 +114,17 @@ class HarvestUserImporter(FromHarvest, rattail_harvest_importing.model.HarvestUs data = super(HarvestUserImporter, self).normalize_host_object(user) if data: + # TODO: for some reason the API used to include the these + # fields, but no longer does as of 2022-11-11, so null is + # kinda the only thing that makes sense now. if possible, + # should figure out "what changed" at Harvest, but maybe + # these fields should just be removed from our cache + # schema? + data.setdefault('is_admin', None) + data.setdefault('is_project_manager', None) + data.setdefault('can_see_rates', None) + data.setdefault('can_create_invoices', None) + if data['telephone'] == '': data['telephone'] = None From 0164336784c48474c7ff962a31ce8412ad15da58 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 17 Jan 2023 16:10:48 -0600 Subject: [PATCH 17/42] Fix date filter for rattail <-> rattail sync of HarvestTimeEntry --- rattail_harvest/importing/rattail.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rattail_harvest/importing/rattail.py b/rattail_harvest/importing/rattail.py index da1e9c6..54ce310 100644 --- a/rattail_harvest/importing/rattail.py +++ b/rattail_harvest/importing/rattail.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -59,4 +59,8 @@ class HarvestTaskImporter(base.FromRattail, rattail_harvest_importing.model.Harv pass class HarvestTimeEntryImporter(base.FromRattail, rattail_harvest_importing.model.HarvestTimeEntryImporter): - pass + + def query(self): + query = super(HarvestTimeEntryImporter, self).query() + return query.filter(self.model_class.spent_date >= self.start_date)\ + .filter(self.model_class.spent_date <= self.end_date) From 44574d9ea635d845f23f9ca9fc12ff702387e250 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 5 May 2023 01:55:53 -0500 Subject: [PATCH 18/42] Avoid deprecated import for `OrderedDict` --- rattail_harvest/importing/harvest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rattail_harvest/importing/harvest.py b/rattail_harvest/importing/harvest.py index 6153348..348c24b 100644 --- a/rattail_harvest/importing/harvest.py +++ b/rattail_harvest/importing/harvest.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -27,11 +27,11 @@ Harvest -> Rattail "cache" data import import datetime import decimal import logging +from collections import OrderedDict import sqlalchemy as sa from rattail import importing -from rattail.util import OrderedDict from rattail_harvest import importing as rattail_harvest_importing from rattail_harvest.harvest.webapi import make_harvest_webapi From d508ca225b2b5b3f8b49ef7c83918c029ac35009 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 16 May 2023 13:09:08 -0500 Subject: [PATCH 19/42] Replace `setup.py` contents with `setup.cfg` --- setup.cfg | 42 +++++++++++++++++++++++++++ setup.py | 87 ++----------------------------------------------------- 2 files changed, 45 insertions(+), 84 deletions(-) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..f552806 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,42 @@ +# -*- coding: utf-8; -*- + +[metadata] +name = rattail-harvest +version = attr: rattail_harvest.__version__ +author = Lance Edgar +author_email = lance@edbob.org +url = https://rattailproject.org/ +description = Rattail integration package for Harvest +long_description = file: README.rst +classifiers = + Development Status :: 3 - Alpha + Environment :: Console + 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 + + +[options] +install_requires = + invoke + rattail[db] + +packages = find: +include_package_data = True + + +[options.entry_points] + +rattail.commands = + import-harvest = rattail_harvest.commands:ImportHarvest + +rattail.config.extensions = + rattail_harvest = rattail_harvest.config:RattailHarvestExtension + +rattail.importing = + to_rattail.from_harvest.import = rattail_harvest.importing.harvest:FromHarvestToRattail diff --git a/setup.py b/setup.py index 0d5a699..71a5a0a 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -24,87 +24,6 @@ rattail-harvest setup script """ -import os -from setuptools import setup, find_packages +from setuptools import setup - -here = os.path.abspath(os.path.dirname(__file__)) -exec(open(os.path.join(here, 'rattail_harvest', '_version.py')).read()) -README = open(os.path.join(here, 'README.rst')).read() - - -requires = [ - # - # Version numbers within comments below have specific meanings. - # Basically the 'low' value is a "soft low," and 'high' a "soft high." - # In other words: - # - # If either a 'low' or 'high' value exists, the primary point to be - # made about the value is that it represents the most current (stable) - # version available for the package (assuming typical public access - # methods) whenever this project was started and/or documented. - # Therefore: - # - # If a 'low' version is present, you should know that attempts to use - # versions of the package significantly older than the 'low' version - # may not yield happy results. (A "hard" high limit may or may not be - # indicated by a true version requirement.) - # - # Similarly, if a 'high' version is present, and especially if this - # project has laid dormant for a while, you may need to refactor a bit - # when attempting to support a more recent version of the package. (A - # "hard" low limit should be indicated by a true version requirement - # when a 'high' version is present.) - # - # In any case, developers and other users are encouraged to play - # outside the lines with regard to these soft limits. If bugs are - # encountered then they should be filed as such. - # - # package # low high - - 'invoke', # 1.5.0 - 'rattail[db]', # 0.9.246 -] - - -setup( - name = "rattail-harvest", - version = __version__, - author = "Lance Edgar", - author_email = "lance@edbob.org", - url = "https://rattailproject.org/", - description = "Rattail integration package for Harvest", - long_description = README, - - classifiers = [ - 'Development Status :: 3 - Alpha', - 'Environment :: Console', - '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 = requires, - packages = find_packages(), - include_package_data = True, - - entry_points = { - - 'rattail.commands': [ - 'import-harvest = rattail_harvest.commands:ImportHarvest', - ], - - 'rattail.config.extensions': [ - 'rattail_harvest = rattail_harvest.config:RattailHarvestExtension', - ], - - 'rattail.importing': [ - 'to_rattail.from_harvest.import = rattail_harvest.importing.harvest:FromHarvestToRattail', - ], - }, -) +setup() From 2fa7ef5e71bf7454394f7860327244e0f510ddc8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 8 Aug 2023 11:05:15 -0500 Subject: [PATCH 20/42] Grow all ID fields for Harvest cache tables turns out Integer is not big enough, need BigInteger --- .../versions/f2a1650e7fbc_grow_id_fields.py | 89 +++++++++++++++++++ rattail_harvest/db/model/harvest.py | 24 ++--- 2 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 rattail_harvest/db/alembic/versions/f2a1650e7fbc_grow_id_fields.py diff --git a/rattail_harvest/db/alembic/versions/f2a1650e7fbc_grow_id_fields.py b/rattail_harvest/db/alembic/versions/f2a1650e7fbc_grow_id_fields.py new file mode 100644 index 0000000..0c24bb5 --- /dev/null +++ b/rattail_harvest/db/alembic/versions/f2a1650e7fbc_grow_id_fields.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8; -*- +"""grow id fields + +Revision ID: f2a1650e7fbc +Revises: 6bc1cb21d920 +Create Date: 2023-08-08 10:53:56.013211 + +""" + +# revision identifiers, used by Alembic. +revision = 'f2a1650e7fbc' +down_revision = '6bc1cb21d920' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa +import rattail.db.types +from sqlalchemy.dialects import postgresql + + +def upgrade(): + + # harvest_user + op.alter_column('harvest_user', 'id', type_=sa.BigInteger()) + op.alter_column('harvest_user_version', 'id', type_=sa.BigInteger()) + + # harvest_client + op.alter_column('harvest_client', 'id', type_=sa.BigInteger()) + op.alter_column('harvest_client_version', 'id', type_=sa.BigInteger()) + + # harvest_project + op.alter_column('harvest_project', 'id', type_=sa.BigInteger()) + op.alter_column('harvest_project', 'client_id', type_=sa.BigInteger()) + op.alter_column('harvest_project_version', 'id', type_=sa.BigInteger()) + op.alter_column('harvest_project_version', 'client_id', type_=sa.BigInteger()) + + # harvest_task + op.alter_column('harvest_task', 'id', type_=sa.BigInteger()) + op.alter_column('harvest_task_version', 'id', type_=sa.BigInteger()) + + # harvest_time_entry + op.alter_column('harvest_time_entry', 'id', type_=sa.BigInteger()) + op.alter_column('harvest_time_entry', 'user_id', type_=sa.BigInteger()) + op.alter_column('harvest_time_entry', 'client_id', type_=sa.BigInteger()) + op.alter_column('harvest_time_entry', 'project_id', type_=sa.BigInteger()) + op.alter_column('harvest_time_entry', 'task_id', type_=sa.BigInteger()) + op.alter_column('harvest_time_entry', 'invoice_id', type_=sa.BigInteger()) + op.alter_column('harvest_time_entry_version', 'id', type_=sa.BigInteger()) + op.alter_column('harvest_time_entry_version', 'user_id', type_=sa.BigInteger()) + op.alter_column('harvest_time_entry_version', 'client_id', type_=sa.BigInteger()) + op.alter_column('harvest_time_entry_version', 'project_id', type_=sa.BigInteger()) + op.alter_column('harvest_time_entry_version', 'task_id', type_=sa.BigInteger()) + op.alter_column('harvest_time_entry_version', 'invoice_id', type_=sa.BigInteger()) + + +def downgrade(): + + # harvest_time_entry + op.alter_column('harvest_time_entry_version', 'id', type_=sa.Integer()) + op.alter_column('harvest_time_entry_version', 'user_id', type_=sa.Integer()) + op.alter_column('harvest_time_entry_version', 'client_id', type_=sa.Integer()) + op.alter_column('harvest_time_entry_version', 'project_id', type_=sa.Integer()) + op.alter_column('harvest_time_entry_version', 'task_id', type_=sa.Integer()) + op.alter_column('harvest_time_entry_version', 'invoice_id', type_=sa.Integer()) + op.alter_column('harvest_time_entry', 'id', type_=sa.Integer()) + op.alter_column('harvest_time_entry', 'user_id', type_=sa.Integer()) + op.alter_column('harvest_time_entry', 'client_id', type_=sa.Integer()) + op.alter_column('harvest_time_entry', 'project_id', type_=sa.Integer()) + op.alter_column('harvest_time_entry', 'task_id', type_=sa.Integer()) + op.alter_column('harvest_time_entry', 'invoice_id', type_=sa.Integer()) + + # harvest_task + op.alter_column('harvest_task_version', 'id', type_=sa.Integer()) + op.alter_column('harvest_task', 'id', type_=sa.Integer()) + + # harvest_project + op.alter_column('harvest_project_version', 'id', type_=sa.Integer()) + op.alter_column('harvest_project_version', 'client_id', type_=sa.Integer()) + op.alter_column('harvest_project', 'id', type_=sa.Integer()) + op.alter_column('harvest_project', 'client_id', type_=sa.Integer()) + + # harvest_client + op.alter_column('harvest_client_version', 'id', type_=sa.Integer()) + op.alter_column('harvest_client', 'id', type_=sa.Integer()) + + # harvest_user + op.alter_column('harvest_user_version', 'id', type_=sa.Integer()) + op.alter_column('harvest_user', 'id', type_=sa.Integer()) diff --git a/rattail_harvest/db/model/harvest.py b/rattail_harvest/db/model/harvest.py index 7ac7506..fd3a8ee 100644 --- a/rattail_harvest/db/model/harvest.py +++ b/rattail_harvest/db/model/harvest.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -46,7 +46,7 @@ class HarvestUser(model.Base): uuid = model.uuid_column() - id = sa.Column(sa.Integer(), nullable=False) + id = sa.Column(sa.BigInteger(), nullable=False) first_name = sa.Column(sa.String(length=255), nullable=True) @@ -122,7 +122,7 @@ class HarvestClient(model.Base): uuid = model.uuid_column() - id = sa.Column(sa.Integer(), nullable=False) + id = sa.Column(sa.BigInteger(), nullable=False) name = sa.Column(sa.String(length=255), nullable=True) @@ -155,9 +155,9 @@ class HarvestProject(model.Base): uuid = model.uuid_column() - id = sa.Column(sa.Integer(), nullable=False) + id = sa.Column(sa.BigInteger(), nullable=False) - client_id = sa.Column(sa.Integer(), nullable=True) # TODO: should not allow null? + client_id = sa.Column(sa.BigInteger(), nullable=True) # TODO: should not allow null? client = orm.relationship(HarvestClient, backref=orm.backref('projects')) name = sa.Column(sa.String(length=255), nullable=True) @@ -226,7 +226,7 @@ class HarvestTask(model.Base): uuid = model.uuid_column() - id = sa.Column(sa.Integer(), nullable=False) + id = sa.Column(sa.BigInteger(), nullable=False) name = sa.Column(sa.String(length=255), nullable=True) @@ -265,23 +265,23 @@ class HarvestTimeEntry(model.Base): uuid = model.uuid_column() - id = sa.Column(sa.Integer(), nullable=False) + id = sa.Column(sa.BigInteger(), nullable=False) spent_date = sa.Column(sa.Date(), nullable=True) - user_id = sa.Column(sa.Integer(), nullable=True) + user_id = sa.Column(sa.BigInteger(), nullable=True) user = orm.relationship(HarvestUser, backref=orm.backref('time_entries')) - client_id = sa.Column(sa.Integer(), nullable=True) + client_id = sa.Column(sa.BigInteger(), nullable=True) client = orm.relationship(HarvestClient, backref=orm.backref('time_entries')) - project_id = sa.Column(sa.Integer(), nullable=True) + project_id = sa.Column(sa.BigInteger(), nullable=True) project = orm.relationship(HarvestProject, backref=orm.backref('time_entries')) - task_id = sa.Column(sa.Integer(), nullable=True) + task_id = sa.Column(sa.BigInteger(), nullable=True) task = orm.relationship(HarvestTask, backref=orm.backref('time_entries')) - invoice_id = sa.Column(sa.Integer(), nullable=True) + invoice_id = sa.Column(sa.BigInteger(), nullable=True) hours = sa.Column(sa.Numeric(precision=6, scale=2), nullable=True) From 2f21e574aecb89b3216abe8d539e3e90ad7bef2d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 24 Sep 2023 09:23:31 -0500 Subject: [PATCH 21/42] Fix start/end date defaults for importers, per upstream changes --- rattail_harvest/importing/model.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/rattail_harvest/importing/model.py b/rattail_harvest/importing/model.py index 086965c..715f5a8 100644 --- a/rattail_harvest/importing/model.py +++ b/rattail_harvest/importing/model.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -67,9 +67,14 @@ class HarvestTimeEntryImporter(ToRattail): self.session, model.HarvestProject, key='id') def cache_query(self): - query = super(HarvestTimeEntryImporter, self).cache_query() - return query.filter(self.model_class.spent_date >= self.start_date)\ - .filter(self.model_class.spent_date <= self.end_date) + query = super().cache_query() + + if self.start_date: + query = query.filter(self.model_class.spent_date >= self.start_date) + if self.end_date: + query = query.filter(self.model_class.spent_date <= self.end_date) + + return query def get_harvest_project(self, project_id): if hasattr(self, 'harvest_projects_by_id'): From e58d843ee43f4f8ac1505ba07b29ac562c371dbc Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 25 Sep 2023 13:26:58 -0500 Subject: [PATCH 22/42] Fix (more) start/end date defaults for importers, per upstream changes --- rattail_harvest/harvest/importing/model.py | 12 ++++++++---- rattail_harvest/importing/harvest.py | 8 ++++++-- rattail_harvest/importing/rattail.py | 11 ++++++++--- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/rattail_harvest/harvest/importing/model.py b/rattail_harvest/harvest/importing/model.py index d445def..3a874c5 100644 --- a/rattail_harvest/harvest/importing/model.py +++ b/rattail_harvest/harvest/importing/model.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -60,11 +60,15 @@ class TimeEntryImporter(ToHarvest): Fetch existing time entries from Harvest. """ cache = {} + # TODO: we try to avoid entries w/ timer still running here, # but for some reason they still come back, so double-check - entries = self.webapi.get_time_entries(**{'from': self.start_date, - 'to': self.end_date, - 'is_running': False}) + kw = {'is_running': False} + if self.start_date: + kw['from'] = self.start_date + if self.end_date: + kw['to'] = self.end_date + entries = self.webapi.get_time_entries(**kw) for entry in entries: # double-check here if not entry['is_running']: diff --git a/rattail_harvest/importing/harvest.py b/rattail_harvest/importing/harvest.py index 348c24b..1e706a2 100644 --- a/rattail_harvest/importing/harvest.py +++ b/rattail_harvest/importing/harvest.py @@ -226,8 +226,12 @@ class HarvestTimeEntryImporter(FromHarvest, rattail_harvest_importing.model.Harv """ def get_host_objects(self): - return self.webapi.get_time_entries(**{'from': self.start_date, - 'to': self.end_date}) + kw = {} + if self.start_date: + kw['from'] = self.start_date + if self.end_date: + kw['to'] = self.end_date + return self.webapi.get_time_entries(**kw) def normalize_host_object(self, entry): data = super(HarvestTimeEntryImporter, self).normalize_host_object(entry) diff --git a/rattail_harvest/importing/rattail.py b/rattail_harvest/importing/rattail.py index 54ce310..d61f3d8 100644 --- a/rattail_harvest/importing/rattail.py +++ b/rattail_harvest/importing/rattail.py @@ -61,6 +61,11 @@ class HarvestTaskImporter(base.FromRattail, rattail_harvest_importing.model.Harv class HarvestTimeEntryImporter(base.FromRattail, rattail_harvest_importing.model.HarvestTimeEntryImporter): def query(self): - query = super(HarvestTimeEntryImporter, self).query() - return query.filter(self.model_class.spent_date >= self.start_date)\ - .filter(self.model_class.spent_date <= self.end_date) + query = super().query() + + if self.start_date: + query = query.filter(self.model_class.spent_date >= self.start_date) + if self.end_date: + query = query.filter(self.model_class.spent_date <= self.end_date) + + return query From 509405cb346f90cd22df6c521850f0ef4af35fea Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 4 Oct 2023 13:08:29 -0500 Subject: [PATCH 23/42] Let Harvest API -> Harvest cache importer fetch single host object for one-off imports via web app --- rattail_harvest/importing/harvest.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rattail_harvest/importing/harvest.py b/rattail_harvest/importing/harvest.py index 1e706a2..1da4cd2 100644 --- a/rattail_harvest/importing/harvest.py +++ b/rattail_harvest/importing/harvest.py @@ -233,6 +233,11 @@ class HarvestTimeEntryImporter(FromHarvest, rattail_harvest_importing.model.Harv kw['to'] = self.end_date return self.webapi.get_time_entries(**kw) + def get_single_host_object(self, key): + assert len(self.key) == 1 and self.key[0] == 'id' + entry_id = key[0] + return self.webapi.get_time_entry(entry_id) + def normalize_host_object(self, entry): data = super(HarvestTimeEntryImporter, self).normalize_host_object(entry) if not data: From aa87ce57bedfce6003041d61ade51fa2aa5f6f45 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 4 Oct 2023 15:54:52 -0500 Subject: [PATCH 24/42] Rename all tables/models for Harvest "cache" make this more explicit, for better naming convention --- .../53c066772ad5_rename_cache_tables.py | 179 ++++++++++++++++++ rattail_harvest/db/model/__init__.py | 8 +- rattail_harvest/db/model/harvest.py | 105 +++++++--- rattail_harvest/importing/harvest.py | 36 ++-- rattail_harvest/importing/model.py | 32 ++-- rattail_harvest/importing/rattail.py | 20 +- rattail_harvest/importing/versions.py | 32 ++-- 7 files changed, 324 insertions(+), 88 deletions(-) create mode 100644 rattail_harvest/db/alembic/versions/53c066772ad5_rename_cache_tables.py diff --git a/rattail_harvest/db/alembic/versions/53c066772ad5_rename_cache_tables.py b/rattail_harvest/db/alembic/versions/53c066772ad5_rename_cache_tables.py new file mode 100644 index 0000000..b185837 --- /dev/null +++ b/rattail_harvest/db/alembic/versions/53c066772ad5_rename_cache_tables.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8; -*- +"""rename cache tables + +Revision ID: 53c066772ad5 +Revises: f2a1650e7fbc +Create Date: 2023-10-04 15:19:03.857323 + +""" + +# revision identifiers, used by Alembic. +revision = '53c066772ad5' +down_revision = 'f2a1650e7fbc' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa +import rattail.db.types + + + +def upgrade(): + + ############################## + # drop all constraints + ############################## + + # harvest_time_entry + op.drop_constraint('harvest_time_entry_fk_user', 'harvest_time_entry', type_='foreignkey') + op.drop_constraint('harvest_time_entry_fk_client', 'harvest_time_entry', type_='foreignkey') + op.drop_constraint('harvest_time_entry_fk_project', 'harvest_time_entry', type_='foreignkey') + op.drop_constraint('harvest_time_entry_fk_task', 'harvest_time_entry', type_='foreignkey') + op.drop_constraint('harvest_time_entry_uq_id', 'harvest_time_entry', type_='unique') + + # harvest_task + op.drop_constraint('harvest_task_uq_id', 'harvest_task', type_='unique') + + # harvest_project + op.drop_constraint('harvest_project_fk_client', 'harvest_project', type_='foreignkey') + op.drop_constraint('harvest_project_uq_id', 'harvest_project', type_='unique') + + # harvest_client + op.drop_constraint('harvest_client_uq_id', 'harvest_client', type_='unique') + + # harvest_user + op.drop_constraint('harvest_user_fk_person', 'harvest_user', type_='foreignkey') + op.drop_constraint('harvest_user_uq_id', 'harvest_user', type_='unique') + + ############################## + # rename all tables + ############################## + + op.rename_table('harvest_user', 'harvest_cache_user') + op.rename_table('harvest_user_version', 'harvest_cache_user_version') + op.rename_table('harvest_client', 'harvest_cache_client') + op.rename_table('harvest_client_version', 'harvest_cache_client_version') + op.rename_table('harvest_project', 'harvest_cache_project') + op.rename_table('harvest_project_version', 'harvest_cache_project_version') + op.rename_table('harvest_task', 'harvest_cache_task') + op.rename_table('harvest_task_version', 'harvest_cache_task_version') + op.rename_table('harvest_time_entry', 'harvest_cache_time_entry') + op.rename_table('harvest_time_entry_version', 'harvest_cache_time_entry_version') + + ############################## + # re-create all constraints + ############################## + + # harvest_cache_user + op.create_foreign_key('harvest_cache_user_fk_person', + 'harvest_cache_user', 'person', + ['person_uuid'], ['uuid']) + op.create_unique_constraint('harvest_cache_user_uq_id', 'harvest_cache_user', ['id']) + + # harvest_cache_client + op.create_unique_constraint('harvest_cache_client_uq_id', 'harvest_cache_client', ['id']) + + # harvest_cache_project + op.create_foreign_key('harvest_cache_project_fk_client', + 'harvest_cache_project', 'harvest_cache_client', + ['client_id'], ['id']) + op.create_unique_constraint('harvest_cache_project_uq_id', 'harvest_cache_project', ['id']) + + # harvest_cache_task + op.create_unique_constraint('harvest_cache_task_uq_id', 'harvest_cache_task', ['id']) + + # harvest_cache_time_entry + op.create_foreign_key('harvest_cache_time_entry_fk_user', + 'harvest_cache_time_entry', 'harvest_cache_user', + ['user_id'], ['id']) + op.create_foreign_key('harvest_cache_time_entry_fk_client', + 'harvest_cache_time_entry', 'harvest_cache_client', + ['client_id'], ['id']) + op.create_foreign_key('harvest_cache_time_entry_fk_project', + 'harvest_cache_time_entry', 'harvest_cache_project', + ['project_id'], ['id']) + op.create_foreign_key('harvest_cache_time_entry_fk_task', + 'harvest_cache_time_entry', 'harvest_cache_task', + ['task_id'], ['id']) + op.create_unique_constraint('harvest_cache_time_entry_uq_id', 'harvest_cache_time_entry', ['id']) + + +def downgrade(): + + ############################## + # drop all constraints + ############################## + + # harvest_cache_time_entry + op.drop_constraint('harvest_cache_time_entry_fk_user', 'harvest_cache_time_entry', type_='foreignkey') + op.drop_constraint('harvest_cache_time_entry_fk_client', 'harvest_cache_time_entry', type_='foreignkey') + op.drop_constraint('harvest_cache_time_entry_fk_project', 'harvest_cache_time_entry', type_='foreignkey') + op.drop_constraint('harvest_cache_time_entry_fk_task', 'harvest_cache_time_entry', type_='foreignkey') + op.drop_constraint('harvest_cache_time_entry_uq_id', 'harvest_cache_time_entry', type_='unique') + + # harvest_cache_task + op.drop_constraint('harvest_cache_task_uq_id', 'harvest_cache_task', type_='unique') + + # harvest_cache_project + op.drop_constraint('harvest_cache_project_fk_client', 'harvest_cache_project', type_='foreignkey') + op.drop_constraint('harvest_cache_project_uq_id', 'harvest_cache_project', type_='unique') + + # harvest_cache_client + op.drop_constraint('harvest_cache_client_uq_id', 'harvest_cache_client', type_='unique') + + # harvest_cache_user + op.drop_constraint('harvest_cache_user_fk_person', 'harvest_cache_user', type_='foreignkey') + op.drop_constraint('harvest_cache_user_uq_id', 'harvest_cache_user', type_='unique') + + ############################## + # rename all tables + ############################## + + op.rename_table('harvest_cache_user', 'harvest_user') + op.rename_table('harvest_cache_user_version', 'harvest_user_version') + op.rename_table('harvest_cache_client', 'harvest_client') + op.rename_table('harvest_cache_client_version', 'harvest_client_version') + op.rename_table('harvest_cache_project', 'harvest_project') + op.rename_table('harvest_cache_project_version', 'harvest_project_version') + op.rename_table('harvest_cache_task', 'harvest_task') + op.rename_table('harvest_cache_task_version', 'harvest_task_version') + op.rename_table('harvest_cache_time_entry', 'harvest_time_entry') + op.rename_table('harvest_cache_time_entry_version', 'harvest_time_entry_version') + + ############################## + # re-create all constraints + ############################## + + # harvest_user + op.create_foreign_key('harvest_user_fk_person', + 'harvest_user', 'person', + ['person_uuid'], ['uuid']) + op.create_unique_constraint('harvest_user_uq_id', 'harvest_user', ['id']) + + # harvest_client + op.create_unique_constraint('harvest_client_uq_id', 'harvest_client', ['id']) + + # harvest_project + op.create_foreign_key('harvest_project_fk_client', + 'harvest_project', 'harvest_client', + ['client_id'], ['id']) + op.create_unique_constraint('harvest_project_uq_id', 'harvest_project', ['id']) + + # harvest_cache_task + op.create_unique_constraint('harvest_task_uq_id', 'harvest_task', ['id']) + + # harvest_time_entry + op.create_foreign_key('harvest_time_entry_fk_user', + 'harvest_time_entry', 'harvest_user', + ['user_id'], ['id']) + op.create_foreign_key('harvest_time_entry_fk_client', + 'harvest_time_entry', 'harvest_client', + ['client_id'], ['id']) + op.create_foreign_key('harvest_time_entry_fk_project', + 'harvest_time_entry', 'harvest_project', + ['project_id'], ['id']) + op.create_foreign_key('harvest_time_entry_fk_task', + 'harvest_time_entry', 'harvest_task', + ['task_id'], ['id']) + op.create_unique_constraint('harvest_time_entry_uq_id', 'harvest_time_entry', ['id']) diff --git a/rattail_harvest/db/model/__init__.py b/rattail_harvest/db/model/__init__.py index e55b2a1..0d58bc1 100644 --- a/rattail_harvest/db/model/__init__.py +++ b/rattail_harvest/db/model/__init__.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -24,5 +24,9 @@ Harvest integration data models """ -from .harvest import (HarvestUser, HarvestClient, HarvestProject, +from .harvest import (HarvestCacheUser, HarvestCacheClient, + HarvestCacheProject, HarvestCacheTask, + HarvestCacheTimeEntry, + # TODO: deprecate / remove these + HarvestUser, HarvestClient, HarvestProject, HarvestTask, HarvestTimeEntry) diff --git a/rattail_harvest/db/model/harvest.py b/rattail_harvest/db/model/harvest.py index fd3a8ee..f3fec9b 100644 --- a/rattail_harvest/db/model/harvest.py +++ b/rattail_harvest/db/model/harvest.py @@ -24,6 +24,8 @@ Harvest "cache" data models """ +import warnings + import sqlalchemy as sa from sqlalchemy import orm @@ -31,16 +33,17 @@ from rattail.db import model from rattail.db.util import normalize_full_name -class HarvestUser(model.Base): +class HarvestCacheUser(model.Base): """ Represents a user record in Harvest. https://help.getharvest.com/api-v2/users-api/users/users/#the-user-object """ - __tablename__ = 'harvest_user' + __tablename__ = 'harvest_cache_user' __table_args__ = ( - sa.ForeignKeyConstraint(['person_uuid'], ['person.uuid'], name='harvest_user_fk_person'), - sa.UniqueConstraint('id', name='harvest_user_uq_id'), + sa.ForeignKeyConstraint(['person_uuid'], ['person.uuid'], + name='harvest_cache_user_fk_person'), + sa.UniqueConstraint('id', name='harvest_cache_user_uq_id'), ) __versioned__ = {} @@ -108,15 +111,24 @@ class HarvestUser(model.Base): return normalize_full_name(self.first_name, self.last_name) -class HarvestClient(model.Base): +class HarvestUser(HarvestCacheUser): + """ DEPRECATED """ + + def __init__(self, *args, **kwargs): + warnings.warn("HarvestUser class is deprecated; " + "please use HarvestCacheUser instead", + DeprecationWarning, stacklevel=2) + + +class HarvestCacheClient(model.Base): """ Represents a client record in Harvest. https://help.getharvest.com/api-v2/clients-api/clients/clients/#the-client-object """ - __tablename__ = 'harvest_client' + __tablename__ = 'harvest_cache_client' __table_args__ = ( - sa.UniqueConstraint('id', name='harvest_client_uq_id'), + sa.UniqueConstraint('id', name='harvest_cache_client_uq_id'), ) __versioned__ = {} @@ -140,16 +152,26 @@ class HarvestClient(model.Base): return self.name or '' -class HarvestProject(model.Base): +class HarvestClient(HarvestCacheClient): + """ DEPRECATED """ + + def __init__(self, *args, **kwargs): + warnings.warn("HarvestClient class is deprecated; " + "please use HarvestCacheClient instead", + DeprecationWarning, stacklevel=2) + + +class HarvestCacheProject(model.Base): """ Represents a project record in Harvest. https://help.getharvest.com/api-v2/projects-api/projects/projects/#the-project-object """ - __tablename__ = 'harvest_project' + __tablename__ = 'harvest_cache_project' __table_args__ = ( - sa.UniqueConstraint('id', name='harvest_project_uq_id'), - sa.ForeignKeyConstraint(['client_id'], ['harvest_client.id'], name='harvest_project_fk_client'), + sa.UniqueConstraint('id', name='harvest_cache_project_uq_id'), + sa.ForeignKeyConstraint(['client_id'], ['harvest_cache_client.id'], + name='harvest_cache_project_fk_client'), ) __versioned__ = {'exclude': ['over_budget_notification_date']} @@ -158,7 +180,7 @@ class HarvestProject(model.Base): id = sa.Column(sa.BigInteger(), nullable=False) client_id = sa.Column(sa.BigInteger(), nullable=True) # TODO: should not allow null? - client = orm.relationship(HarvestClient, backref=orm.backref('projects')) + client = orm.relationship(HarvestCacheClient, backref=orm.backref('projects')) name = sa.Column(sa.String(length=255), nullable=True) @@ -212,15 +234,24 @@ class HarvestProject(model.Base): return self.name or '' -class HarvestTask(model.Base): +class HarvestProject(HarvestCacheProject): + """ DEPRECATED """ + + def __init__(self, *args, **kwargs): + warnings.warn("HarvestProject class is deprecated; " + "please use HarvestCacheProject instead", + DeprecationWarning, stacklevel=2) + + +class HarvestCacheTask(model.Base): """ Represents a task record in Harvest. https://help.getharvest.com/api-v2/tasks-api/tasks/tasks/#the-task-object """ - __tablename__ = 'harvest_task' + __tablename__ = 'harvest_cache_task' __table_args__ = ( - sa.UniqueConstraint('id', name='harvest_task_uq_id'), + sa.UniqueConstraint('id', name='harvest_cache_task_uq_id'), ) __versioned__ = {} @@ -246,19 +277,32 @@ class HarvestTask(model.Base): return self.name or '' -class HarvestTimeEntry(model.Base): +class HarvestTask(HarvestCacheTask): + """ DEPRECATED """ + + def __init__(self, *args, **kwargs): + warnings.warn("HarvestTask class is deprecated; " + "please use HarvestCacheTask instead", + DeprecationWarning, stacklevel=2) + + +class HarvestCacheTimeEntry(model.Base): """ Represents a time entry record in Harvest. https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/#the-time-entry-object """ - __tablename__ = 'harvest_time_entry' + __tablename__ = 'harvest_cache_time_entry' __table_args__ = ( - sa.UniqueConstraint('id', name='harvest_time_entry_uq_id'), - sa.ForeignKeyConstraint(['user_id'], ['harvest_user.id'], name='harvest_time_entry_fk_user'), - sa.ForeignKeyConstraint(['client_id'], ['harvest_client.id'], name='harvest_time_entry_fk_client'), - sa.ForeignKeyConstraint(['project_id'], ['harvest_project.id'], name='harvest_time_entry_fk_project'), - sa.ForeignKeyConstraint(['task_id'], ['harvest_task.id'], name='harvest_time_entry_fk_task'), + sa.UniqueConstraint('id', name='harvest_cache_time_entry_uq_id'), + sa.ForeignKeyConstraint(['user_id'], ['harvest_cache_user.id'], + name='harvest_cache_time_entry_fk_user'), + sa.ForeignKeyConstraint(['client_id'], ['harvest_cache_client.id'], + name='harvest_cache_time_entry_fk_client'), + sa.ForeignKeyConstraint(['project_id'], ['harvest_cache_project.id'], + name='harvest_cache_time_entry_fk_project'), + sa.ForeignKeyConstraint(['task_id'], ['harvest_cache_task.id'], + name='harvest_cache_time_entry_fk_task'), ) __versioned__ = {} model_title_plural = "Harvest Time Entries" @@ -270,16 +314,16 @@ class HarvestTimeEntry(model.Base): spent_date = sa.Column(sa.Date(), nullable=True) user_id = sa.Column(sa.BigInteger(), nullable=True) - user = orm.relationship(HarvestUser, backref=orm.backref('time_entries')) + user = orm.relationship(HarvestCacheUser, backref=orm.backref('time_entries')) client_id = sa.Column(sa.BigInteger(), nullable=True) - client = orm.relationship(HarvestClient, backref=orm.backref('time_entries')) + client = orm.relationship(HarvestCacheClient, backref=orm.backref('time_entries')) project_id = sa.Column(sa.BigInteger(), nullable=True) - project = orm.relationship(HarvestProject, backref=orm.backref('time_entries')) + project = orm.relationship(HarvestCacheProject, backref=orm.backref('time_entries')) task_id = sa.Column(sa.BigInteger(), nullable=True) - task = orm.relationship(HarvestTask, backref=orm.backref('time_entries')) + task = orm.relationship(HarvestCacheTask, backref=orm.backref('time_entries')) invoice_id = sa.Column(sa.BigInteger(), nullable=True) @@ -317,3 +361,12 @@ class HarvestTimeEntry(model.Base): def __str__(self): return str(self.spent_date or '') + + +class HarvestTimeEntry(HarvestCacheTimeEntry): + """ DEPRECATED """ + + def __init__(self, *args, **kwargs): + warnings.warn("HarvestTimeEntry class is deprecated; " + "please use HarvestCacheTimeEntry instead", + DeprecationWarning, stacklevel=2) diff --git a/rattail_harvest/importing/harvest.py b/rattail_harvest/importing/harvest.py index 1da4cd2..4f22ef1 100644 --- a/rattail_harvest/importing/harvest.py +++ b/rattail_harvest/importing/harvest.py @@ -49,11 +49,11 @@ class FromHarvestToRattail(importing.ToRattailHandler): def get_importers(self): importers = OrderedDict() - importers['HarvestUser'] = HarvestUserImporter - importers['HarvestClient'] = HarvestClientImporter - importers['HarvestProject'] = HarvestProjectImporter - importers['HarvestTask'] = HarvestTaskImporter - importers['HarvestTimeEntry'] = HarvestTimeEntryImporter + importers['HarvestCacheUser'] = HarvestCacheUserImporter + importers['HarvestCacheClient'] = HarvestCacheClientImporter + importers['HarvestCacheProject'] = HarvestCacheProjectImporter + importers['HarvestCacheTask'] = HarvestCacheTaskImporter + importers['HarvestCacheTimeEntry'] = HarvestCacheTimeEntryImporter return importers @@ -90,14 +90,14 @@ class FromHarvest(importing.Importer): return data -class HarvestUserImporter(FromHarvest, rattail_harvest_importing.model.HarvestUserImporter): +class HarvestCacheUserImporter(FromHarvest, rattail_harvest_importing.model.HarvestCacheUserImporter): """ Import user data from Harvest """ @property def supported_fields(self): - fields = list(super(HarvestUserImporter, self).supported_fields) + fields = list(super().supported_fields) # this is for local tracking only; is not in harvest fields.remove('person_uuid') @@ -111,7 +111,7 @@ class HarvestUserImporter(FromHarvest, rattail_harvest_importing.model.HarvestUs return self.webapi.get_users()['users'] def normalize_host_object(self, user): - data = super(HarvestUserImporter, self).normalize_host_object(user) + data = super().normalize_host_object(user) if data: # TODO: for some reason the API used to include the these @@ -131,7 +131,7 @@ class HarvestUserImporter(FromHarvest, rattail_harvest_importing.model.HarvestUs return data -class HarvestClientImporter(FromHarvest, rattail_harvest_importing.model.HarvestClientImporter): +class HarvestCacheClientImporter(FromHarvest, rattail_harvest_importing.model.HarvestCacheClientImporter): """ Import client data from Harvest """ @@ -140,14 +140,14 @@ class HarvestClientImporter(FromHarvest, rattail_harvest_importing.model.Harvest return self.webapi.get_clients()['clients'] -class HarvestProjectImporter(FromHarvest, rattail_harvest_importing.model.HarvestProjectImporter): +class HarvestCacheProjectImporter(FromHarvest, rattail_harvest_importing.model.HarvestCacheProjectImporter): """ Import project data from Harvest """ @property def supported_fields(self): - fields = list(super(HarvestProjectImporter, self).supported_fields) + fields = list(super().supported_fields) # this is for local tracking only; is not in harvest fields.remove('deleted') @@ -156,16 +156,16 @@ class HarvestProjectImporter(FromHarvest, rattail_harvest_importing.model.Harves def cache_query(self): model = self.model - return self.session.query(model.HarvestProject)\ + return self.session.query(model.HarvestCacheProject)\ .filter(sa.or_( - model.HarvestProject.deleted == False, - model.HarvestProject.deleted == None)) + model.HarvestCacheProject.deleted == False, + model.HarvestCacheProject.deleted == None)) def get_host_objects(self): return self.webapi.get_projects() def normalize_host_object(self, project): - data = super(HarvestProjectImporter, self).normalize_host_object(project) + data = super().normalize_host_object(project) if not data: return @@ -211,7 +211,7 @@ class HarvestProjectImporter(FromHarvest, rattail_harvest_importing.model.Harves return True -class HarvestTaskImporter(FromHarvest, rattail_harvest_importing.model.HarvestTaskImporter): +class HarvestCacheTaskImporter(FromHarvest, rattail_harvest_importing.model.HarvestCacheTaskImporter): """ Import task data from Harvest """ @@ -220,7 +220,7 @@ class HarvestTaskImporter(FromHarvest, rattail_harvest_importing.model.HarvestTa return self.webapi.get_tasks()['tasks'] -class HarvestTimeEntryImporter(FromHarvest, rattail_harvest_importing.model.HarvestTimeEntryImporter): +class HarvestCacheTimeEntryImporter(FromHarvest, rattail_harvest_importing.model.HarvestCacheTimeEntryImporter): """ Import time entry data from Harvest """ @@ -239,7 +239,7 @@ class HarvestTimeEntryImporter(FromHarvest, rattail_harvest_importing.model.Harv return self.webapi.get_time_entry(entry_id) def normalize_host_object(self, entry): - data = super(HarvestTimeEntryImporter, self).normalize_host_object(entry) + data = super().normalize_host_object(entry) if not data: return diff --git a/rattail_harvest/importing/model.py b/rattail_harvest/importing/model.py index 715f5a8..5e35839 100644 --- a/rattail_harvest/importing/model.py +++ b/rattail_harvest/importing/model.py @@ -37,20 +37,20 @@ log = logging.getLogger(__name__) # harvest cache models ############################## -class HarvestUserImporter(ToRattail): - model_class = model.HarvestUser +class HarvestCacheUserImporter(ToRattail): + model_class = model.HarvestCacheUser -class HarvestClientImporter(ToRattail): - model_class = model.HarvestClient +class HarvestCacheClientImporter(ToRattail): + model_class = model.HarvestCacheClient -class HarvestProjectImporter(ToRattail): - model_class = model.HarvestProject +class HarvestCacheProjectImporter(ToRattail): + model_class = model.HarvestCacheProject -class HarvestTaskImporter(ToRattail): - model_class = model.HarvestTask +class HarvestCacheTaskImporter(ToRattail): + model_class = model.HarvestCacheTask -class HarvestTimeEntryImporter(ToRattail): - model_class = model.HarvestTimeEntry +class HarvestCacheTimeEntryImporter(ToRattail): + model_class = model.HarvestCacheTimeEntry # flags to auto-create records for "unknown" references auto_create_unknown_project = True @@ -59,12 +59,12 @@ class HarvestTimeEntryImporter(ToRattail): warn_for_unknown_project = True def setup(self): - super(HarvestTimeEntryImporter, self).setup() + super().setup() model = self.model if 'project_id' in self.fields: self.harvest_projects_by_id = self.app.cache_model( - self.session, model.HarvestProject, key='id') + self.session, model.HarvestCacheProject, key='id') def cache_query(self): query = super().cache_query() @@ -81,12 +81,12 @@ class HarvestTimeEntryImporter(ToRattail): return self.harvest_projects_by_id.get(project_id) model = self.model - return self.session.query(model.HarvestProject)\ - .filter(model.HarvestProject.id == project_id)\ + return self.session.query(model.HarvestCacheProject)\ + .filter(model.HarvestCacheProject.id == project_id)\ .first() def update_object(self, entry, data, local_data=None): - entry = super(HarvestTimeEntryImporter, self).update_object(entry, data, local_data) + entry = super().update_object(entry, data, local_data) model = self.model if 'project_id' in self.fields: @@ -97,7 +97,7 @@ class HarvestTimeEntryImporter(ToRattail): logger("unknown project id %s for time entry id %s: %s", project_id, entry.id, entry) if self.auto_create_unknown_project: - project = model.HarvestProject() + project = model.HarvestCacheProject() project.id = project_id project.name = "(unknown)" self.session.add(project) diff --git a/rattail_harvest/importing/rattail.py b/rattail_harvest/importing/rattail.py index d61f3d8..ab67489 100644 --- a/rattail_harvest/importing/rattail.py +++ b/rattail_harvest/importing/rattail.py @@ -34,11 +34,11 @@ class FromRattailToRattailHarvestMixin(object): """ def add_harvest_importers(self, importers): - importers['HarvestUser'] = HarvestUserImporter - importers['HarvestClient'] = HarvestClientImporter - importers['HarvestProject'] = HarvestProjectImporter - importers['HarvestTask'] = HarvestTaskImporter - importers['HarvestTimeEntry'] = HarvestTimeEntryImporter + importers['HarvestCacheUser'] = HarvestCacheUserImporter + importers['HarvestCacheClient'] = HarvestCacheClientImporter + importers['HarvestCacheProject'] = HarvestCacheProjectImporter + importers['HarvestCacheTask'] = HarvestCacheTaskImporter + importers['HarvestCacheTimeEntry'] = HarvestCacheTimeEntryImporter return importers @@ -46,19 +46,19 @@ class FromRattailToRattailHarvestMixin(object): # harvest cache models ############################## -class HarvestUserImporter(base.FromRattail, rattail_harvest_importing.model.HarvestUserImporter): +class HarvestCacheUserImporter(base.FromRattail, rattail_harvest_importing.model.HarvestCacheUserImporter): pass -class HarvestClientImporter(base.FromRattail, rattail_harvest_importing.model.HarvestClientImporter): +class HarvestCacheClientImporter(base.FromRattail, rattail_harvest_importing.model.HarvestCacheClientImporter): pass -class HarvestProjectImporter(base.FromRattail, rattail_harvest_importing.model.HarvestProjectImporter): +class HarvestCacheProjectImporter(base.FromRattail, rattail_harvest_importing.model.HarvestCacheProjectImporter): pass -class HarvestTaskImporter(base.FromRattail, rattail_harvest_importing.model.HarvestTaskImporter): +class HarvestCacheTaskImporter(base.FromRattail, rattail_harvest_importing.model.HarvestCacheTaskImporter): pass -class HarvestTimeEntryImporter(base.FromRattail, rattail_harvest_importing.model.HarvestTimeEntryImporter): +class HarvestCacheTimeEntryImporter(base.FromRattail, rattail_harvest_importing.model.HarvestCacheTimeEntryImporter): def query(self): query = super().query() diff --git a/rattail_harvest/importing/versions.py b/rattail_harvest/importing/versions.py index 3dd5571..e82d861 100644 --- a/rattail_harvest/importing/versions.py +++ b/rattail_harvest/importing/versions.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -30,44 +30,44 @@ from rattail.importing import versions as base class HarvestVersionMixin(object): def add_harvest_importers(self, importers): - importers['HarvestUser'] = HarvestUserImporter - importers['HarvestClient'] = HarvestClientImporter - importers['HarvestProject'] = HarvestProjectImporter - importers['HarvestTask'] = HarvestTaskImporter - importers['HarvestTimeEntry'] = HarvestTimeEntryImporter + importers['HarvestCacheUser'] = HarvestCacheUserImporter + importers['HarvestCacheClient'] = HarvestCacheClientImporter + importers['HarvestCacheProject'] = HarvestCacheProjectImporter + importers['HarvestCacheTask'] = HarvestCacheTaskImporter + importers['HarvestCacheTimeEntry'] = HarvestCacheTimeEntryImporter return importers -class HarvestUserImporter(base.VersionImporter): +class HarvestCacheUserImporter(base.VersionImporter): @property def host_model_class(self): - return self.model.HarvestUser + return self.model.HarvestCacheUser -class HarvestClientImporter(base.VersionImporter): +class HarvestCacheClientImporter(base.VersionImporter): @property def host_model_class(self): - return self.model.HarvestClient + return self.model.HarvestCacheClient -class HarvestProjectImporter(base.VersionImporter): +class HarvestCacheProjectImporter(base.VersionImporter): @property def host_model_class(self): - return self.model.HarvestProject + return self.model.HarvestCacheProject -class HarvestTaskImporter(base.VersionImporter): +class HarvestCacheTaskImporter(base.VersionImporter): @property def host_model_class(self): - return self.model.HarvestTask + return self.model.HarvestCacheTask -class HarvestTimeEntryImporter(base.VersionImporter): +class HarvestCacheTimeEntryImporter(base.VersionImporter): @property def host_model_class(self): - return self.model.HarvestTimeEntry + return self.model.HarvestCacheTimeEntry From fe0daf00bc8fe68f863b4468fa7b7d3afbce24e4 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 5 Oct 2023 09:50:12 -0500 Subject: [PATCH 25/42] Add logic to fetch single Harvest time entry via API for one-off imports --- rattail_harvest/harvest/importing/model.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rattail_harvest/harvest/importing/model.py b/rattail_harvest/harvest/importing/model.py index 3a874c5..611680d 100644 --- a/rattail_harvest/harvest/importing/model.py +++ b/rattail_harvest/harvest/importing/model.py @@ -79,6 +79,12 @@ class TimeEntryImporter(ToHarvest): cache[key] = normal return cache + def get_single_local_object(self, key): + assert len(self.key) == 1 and self.key[0] == 'id' + entry_id = key[0] + if entry_id > 0: + return self.webapi.get_time_entry(entry_id) + def normalize_local_object(self, entry): data = { 'id': entry['id'], From bdb8b22ef41824cd52efb8535fa411b07a2b9703 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 8 Oct 2023 20:38:25 -0500 Subject: [PATCH 26/42] Add some support for datasync, and deleting times from Harvest --- rattail_harvest/db/model/__init__.py | 5 +-- rattail_harvest/db/model/harvest.py | 45 ---------------------- rattail_harvest/harvest/importing/model.py | 12 +++++- rattail_harvest/harvest/webapi.py | 26 ++++++++++++- 4 files changed, 35 insertions(+), 53 deletions(-) diff --git a/rattail_harvest/db/model/__init__.py b/rattail_harvest/db/model/__init__.py index 0d58bc1..86a099f 100644 --- a/rattail_harvest/db/model/__init__.py +++ b/rattail_harvest/db/model/__init__.py @@ -26,7 +26,4 @@ Harvest integration data models from .harvest import (HarvestCacheUser, HarvestCacheClient, HarvestCacheProject, HarvestCacheTask, - HarvestCacheTimeEntry, - # TODO: deprecate / remove these - HarvestUser, HarvestClient, HarvestProject, - HarvestTask, HarvestTimeEntry) + HarvestCacheTimeEntry) diff --git a/rattail_harvest/db/model/harvest.py b/rattail_harvest/db/model/harvest.py index f3fec9b..4b015d2 100644 --- a/rattail_harvest/db/model/harvest.py +++ b/rattail_harvest/db/model/harvest.py @@ -111,15 +111,6 @@ class HarvestCacheUser(model.Base): return normalize_full_name(self.first_name, self.last_name) -class HarvestUser(HarvestCacheUser): - """ DEPRECATED """ - - def __init__(self, *args, **kwargs): - warnings.warn("HarvestUser class is deprecated; " - "please use HarvestCacheUser instead", - DeprecationWarning, stacklevel=2) - - class HarvestCacheClient(model.Base): """ Represents a client record in Harvest. @@ -152,15 +143,6 @@ class HarvestCacheClient(model.Base): return self.name or '' -class HarvestClient(HarvestCacheClient): - """ DEPRECATED """ - - def __init__(self, *args, **kwargs): - warnings.warn("HarvestClient class is deprecated; " - "please use HarvestCacheClient instead", - DeprecationWarning, stacklevel=2) - - class HarvestCacheProject(model.Base): """ Represents a project record in Harvest. @@ -234,15 +216,6 @@ class HarvestCacheProject(model.Base): return self.name or '' -class HarvestProject(HarvestCacheProject): - """ DEPRECATED """ - - def __init__(self, *args, **kwargs): - warnings.warn("HarvestProject class is deprecated; " - "please use HarvestCacheProject instead", - DeprecationWarning, stacklevel=2) - - class HarvestCacheTask(model.Base): """ Represents a task record in Harvest. @@ -277,15 +250,6 @@ class HarvestCacheTask(model.Base): return self.name or '' -class HarvestTask(HarvestCacheTask): - """ DEPRECATED """ - - def __init__(self, *args, **kwargs): - warnings.warn("HarvestTask class is deprecated; " - "please use HarvestCacheTask instead", - DeprecationWarning, stacklevel=2) - - class HarvestCacheTimeEntry(model.Base): """ Represents a time entry record in Harvest. @@ -361,12 +325,3 @@ class HarvestCacheTimeEntry(model.Base): def __str__(self): return str(self.spent_date or '') - - -class HarvestTimeEntry(HarvestCacheTimeEntry): - """ DEPRECATED """ - - def __init__(self, *args, **kwargs): - warnings.warn("HarvestTimeEntry class is deprecated; " - "please use HarvestCacheTimeEntry instead", - DeprecationWarning, stacklevel=2) diff --git a/rattail_harvest/harvest/importing/model.py b/rattail_harvest/harvest/importing/model.py index 611680d..eacbeb0 100644 --- a/rattail_harvest/harvest/importing/model.py +++ b/rattail_harvest/harvest/importing/model.py @@ -31,7 +31,14 @@ from rattail_harvest.harvest.webapi import make_harvest_webapi class ToHarvest(importing.Importer): def setup(self): - super(ToHarvest, self).setup() + super().setup() + self.setup_webapi() + + def datasync_setup(self): + super().datasync_setup() + self.setup_webapi() + + def setup_webapi(self): self.webapi = make_harvest_webapi(self.config) @@ -158,4 +165,5 @@ class TimeEntryImporter(ToHarvest): if self.dry_run: return True - raise NotImplementedError + self.webapi.delete_time_entry(entry['id']) + return True diff --git a/rattail_harvest/harvest/webapi.py b/rattail_harvest/harvest/webapi.py index 03e57bd..fafdcb8 100644 --- a/rattail_harvest/harvest/webapi.py +++ b/rattail_harvest/harvest/webapi.py @@ -61,6 +61,9 @@ class HarvestWebAPI(object): elif request_method == 'PATCH': response = requests.patch('{}/{}'.format(self.base_url, api_method), headers=headers, params=params) + elif request_method == 'DELETE': + response = requests.delete('{}/{}'.format(self.base_url, api_method), + headers=headers, params=params) else: raise NotImplementedError("unknown request method: {}".format( request_method)) @@ -85,6 +88,12 @@ class HarvestWebAPI(object): """ return self._request('PATCH', api_method, params=params) + def delete(self, api_method, params=None): + """ + Perform a DELETE request for the given API method, and return the response. + """ + return self._request('DELETE', api_method, params=params) + def get_company(self): """ Retrieves the company for the currently authenticated user. @@ -170,8 +179,13 @@ class HarvestWebAPI(object): https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/#retrieve-a-time-entry """ - response = self.get('/time_entries/{}'.format(time_entry_id)) - return response.json() + try: + response = self.get('/time_entries/{}'.format(time_entry_id)) + except requests.exceptions.HTTPError as error: + if error.response.status_code != 404: + raise + else: + return response.json() def create_time_entry(self, **kwargs): """ @@ -208,6 +222,14 @@ class HarvestWebAPI(object): response = self.patch('/time_entries/{}'.format(time_entry_id), params=kwargs) return response.json() + def delete_time_entry(self, time_entry_id, **kwargs): + """ + Delete a time entry. + + https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/#delete-a-time-entry + """ + self.delete('/time_entries/{}'.format(time_entry_id), params=kwargs) + def make_harvest_webapi(config): access_token = config.require('harvest', 'api.access_token') From 9bf9a17bf6937c91eef530e46284ed510cf59ad4 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 23 Oct 2023 17:37:46 -0500 Subject: [PATCH 27/42] Fix version table index names --- .../versions/a1cf300fb371_fix_indeces.py | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 rattail_harvest/db/alembic/versions/a1cf300fb371_fix_indeces.py diff --git a/rattail_harvest/db/alembic/versions/a1cf300fb371_fix_indeces.py b/rattail_harvest/db/alembic/versions/a1cf300fb371_fix_indeces.py new file mode 100644 index 0000000..fe059e0 --- /dev/null +++ b/rattail_harvest/db/alembic/versions/a1cf300fb371_fix_indeces.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8; -*- +"""fix indeces + +Revision ID: a1cf300fb371 +Revises: 53c066772ad5 +Create Date: 2023-10-23 17:35:15.527740 + +""" + +# revision identifiers, used by Alembic. +revision = 'a1cf300fb371' +down_revision = '53c066772ad5' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa +import rattail.db.types + + + +def upgrade(): + + # harvest_cache_user + op.drop_index('ix_harvest_user_version_end_transaction_id', table_name='harvest_cache_user_version') + op.drop_index('ix_harvest_user_version_operation_type', table_name='harvest_cache_user_version') + op.drop_index('ix_harvest_user_version_transaction_id', table_name='harvest_cache_user_version') + op.create_index(op.f('ix_harvest_cache_user_version_end_transaction_id'), 'harvest_cache_user_version', ['end_transaction_id'], unique=False) + op.create_index(op.f('ix_harvest_cache_user_version_operation_type'), 'harvest_cache_user_version', ['operation_type'], unique=False) + op.create_index(op.f('ix_harvest_cache_user_version_transaction_id'), 'harvest_cache_user_version', ['transaction_id'], unique=False) + + # harvest_cache_client + op.drop_index('ix_harvest_client_version_end_transaction_id', table_name='harvest_cache_client_version') + op.drop_index('ix_harvest_client_version_operation_type', table_name='harvest_cache_client_version') + op.drop_index('ix_harvest_client_version_transaction_id', table_name='harvest_cache_client_version') + op.create_index(op.f('ix_harvest_cache_client_version_end_transaction_id'), 'harvest_cache_client_version', ['end_transaction_id'], unique=False) + op.create_index(op.f('ix_harvest_cache_client_version_operation_type'), 'harvest_cache_client_version', ['operation_type'], unique=False) + op.create_index(op.f('ix_harvest_cache_client_version_transaction_id'), 'harvest_cache_client_version', ['transaction_id'], unique=False) + + # harvest_cache_project + op.drop_index('ix_harvest_project_version_end_transaction_id', table_name='harvest_cache_project_version') + op.drop_index('ix_harvest_project_version_operation_type', table_name='harvest_cache_project_version') + op.drop_index('ix_harvest_project_version_transaction_id', table_name='harvest_cache_project_version') + op.create_index(op.f('ix_harvest_cache_project_version_end_transaction_id'), 'harvest_cache_project_version', ['end_transaction_id'], unique=False) + op.create_index(op.f('ix_harvest_cache_project_version_operation_type'), 'harvest_cache_project_version', ['operation_type'], unique=False) + op.create_index(op.f('ix_harvest_cache_project_version_transaction_id'), 'harvest_cache_project_version', ['transaction_id'], unique=False) + + # harvest_cache_task + op.drop_index('ix_harvest_task_version_end_transaction_id', table_name='harvest_cache_task_version') + op.drop_index('ix_harvest_task_version_operation_type', table_name='harvest_cache_task_version') + op.drop_index('ix_harvest_task_version_transaction_id', table_name='harvest_cache_task_version') + op.create_index(op.f('ix_harvest_cache_task_version_end_transaction_id'), 'harvest_cache_task_version', ['end_transaction_id'], unique=False) + op.create_index(op.f('ix_harvest_cache_task_version_operation_type'), 'harvest_cache_task_version', ['operation_type'], unique=False) + op.create_index(op.f('ix_harvest_cache_task_version_transaction_id'), 'harvest_cache_task_version', ['transaction_id'], unique=False) + + # harvest_cache_time_entry + op.drop_index('ix_harvest_time_entry_version_end_transaction_id', table_name='harvest_cache_time_entry_version') + op.drop_index('ix_harvest_time_entry_version_operation_type', table_name='harvest_cache_time_entry_version') + op.drop_index('ix_harvest_time_entry_version_transaction_id', table_name='harvest_cache_time_entry_version') + op.create_index(op.f('ix_harvest_cache_time_entry_version_end_transaction_id'), 'harvest_cache_time_entry_version', ['end_transaction_id'], unique=False) + op.create_index(op.f('ix_harvest_cache_time_entry_version_operation_type'), 'harvest_cache_time_entry_version', ['operation_type'], unique=False) + op.create_index(op.f('ix_harvest_cache_time_entry_version_transaction_id'), 'harvest_cache_time_entry_version', ['transaction_id'], unique=False) + + +def downgrade(): + + # harvest_cache_time_entry + op.drop_index(op.f('ix_harvest_cache_time_entry_version_transaction_id'), table_name='harvest_cache_time_entry_version') + op.drop_index(op.f('ix_harvest_cache_time_entry_version_operation_type'), table_name='harvest_cache_time_entry_version') + op.drop_index(op.f('ix_harvest_cache_time_entry_version_end_transaction_id'), table_name='harvest_cache_time_entry_version') + op.create_index('ix_harvest_time_entry_version_transaction_id', 'harvest_cache_time_entry_version', ['transaction_id'], unique=False) + op.create_index('ix_harvest_time_entry_version_operation_type', 'harvest_cache_time_entry_version', ['operation_type'], unique=False) + op.create_index('ix_harvest_time_entry_version_end_transaction_id', 'harvest_cache_time_entry_version', ['end_transaction_id'], unique=False) + + # harvest_cache_task + op.drop_index(op.f('ix_harvest_cache_task_version_transaction_id'), table_name='harvest_cache_task_version') + op.drop_index(op.f('ix_harvest_cache_task_version_operation_type'), table_name='harvest_cache_task_version') + op.drop_index(op.f('ix_harvest_cache_task_version_end_transaction_id'), table_name='harvest_cache_task_version') + op.create_index('ix_harvest_task_version_transaction_id', 'harvest_cache_task_version', ['transaction_id'], unique=False) + op.create_index('ix_harvest_task_version_operation_type', 'harvest_cache_task_version', ['operation_type'], unique=False) + op.create_index('ix_harvest_task_version_end_transaction_id', 'harvest_cache_task_version', ['end_transaction_id'], unique=False) + + # harvest_cache_project + op.drop_index(op.f('ix_harvest_cache_project_version_transaction_id'), table_name='harvest_cache_project_version') + op.drop_index(op.f('ix_harvest_cache_project_version_operation_type'), table_name='harvest_cache_project_version') + op.drop_index(op.f('ix_harvest_cache_project_version_end_transaction_id'), table_name='harvest_cache_project_version') + op.create_index('ix_harvest_project_version_transaction_id', 'harvest_cache_project_version', ['transaction_id'], unique=False) + op.create_index('ix_harvest_project_version_operation_type', 'harvest_cache_project_version', ['operation_type'], unique=False) + op.create_index('ix_harvest_project_version_end_transaction_id', 'harvest_cache_project_version', ['end_transaction_id'], unique=False) + + # harvest_cache_client + op.drop_index(op.f('ix_harvest_cache_client_version_transaction_id'), table_name='harvest_cache_client_version') + op.drop_index(op.f('ix_harvest_cache_client_version_operation_type'), table_name='harvest_cache_client_version') + op.drop_index(op.f('ix_harvest_cache_client_version_end_transaction_id'), table_name='harvest_cache_client_version') + op.create_index('ix_harvest_client_version_transaction_id', 'harvest_cache_client_version', ['transaction_id'], unique=False) + op.create_index('ix_harvest_client_version_operation_type', 'harvest_cache_client_version', ['operation_type'], unique=False) + op.create_index('ix_harvest_client_version_end_transaction_id', 'harvest_cache_client_version', ['end_transaction_id'], unique=False) + + # harvest_cache_user + op.drop_index(op.f('ix_harvest_cache_user_version_transaction_id'), table_name='harvest_cache_user_version') + op.drop_index(op.f('ix_harvest_cache_user_version_operation_type'), table_name='harvest_cache_user_version') + op.drop_index(op.f('ix_harvest_cache_user_version_end_transaction_id'), table_name='harvest_cache_user_version') + op.create_index('ix_harvest_user_version_transaction_id', 'harvest_cache_user_version', ['transaction_id'], unique=False) + op.create_index('ix_harvest_user_version_operation_type', 'harvest_cache_user_version', ['operation_type'], unique=False) + op.create_index('ix_harvest_user_version_end_transaction_id', 'harvest_cache_user_version', ['end_transaction_id'], unique=False) From da408a66bbc3e8a7d80a77cf007861adc41859d9 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 18 Nov 2023 23:25:16 -0600 Subject: [PATCH 28/42] Update changelog --- CHANGELOG.md | 4 ++++ rattail_harvest/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cc2c69..092b760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to rattail-harvest 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). +## [0.1.2] - 2023-11-18 +### Changed +- Catch-up release, with various schema changes etc. + ## [0.1.1] - 2022-01-29 ### Added - Initial version. diff --git a/rattail_harvest/_version.py b/rattail_harvest/_version.py index 4984097..de58c20 100644 --- a/rattail_harvest/_version.py +++ b/rattail_harvest/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.1' +__version__ = '0.1.2' From d7e840138bc89ac2aa9d2c956a11283d4e8e5e41 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 23 Nov 2023 06:50:29 -0600 Subject: [PATCH 29/42] Update subcommand entry point group names, per wuttjamaican --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index f552806..0e27e0b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,7 +32,7 @@ include_package_data = True [options.entry_points] -rattail.commands = +rattail.subcommands = import-harvest = rattail_harvest.commands:ImportHarvest rattail.config.extensions = From 8784579e27ba3cafe3a99c4cdb3560c028b92005 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 16 May 2024 19:19:24 -0500 Subject: [PATCH 30/42] Add typer equivalents for `rattail` commands --- rattail_harvest/commands.py | 27 ++++++++++++++++++++++++--- setup.cfg | 3 +++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/rattail_harvest/commands.py b/rattail_harvest/commands.py index 80d5be1..7cc804b 100644 --- a/rattail_harvest/commands.py +++ b/rattail_harvest/commands.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2024 Lance Edgar # # This file is part of Rattail. # @@ -24,10 +24,31 @@ rattail-harvest commands """ -from rattail import commands +import typer + +from rattail.commands import rattail_typer, ImportSubcommand +from rattail.commands.typer import importer_command, typer_get_runas_user +from rattail.commands.importing import ImportCommandHandler -class ImportHarvest(commands.ImportSubcommand): +@rattail_typer.command() +@importer_command +def import_harvest( + ctx: typer.Context, + **kwargs +): + """ + Import data to Rattail, from Harvest API + """ + config = ctx.parent.rattail_config + progress = ctx.parent.rattail_progress + handler = ImportCommandHandler( + config, import_handler_key='to_rattail.from_harvest.import') + kwargs['user'] = typer_get_runas_user(ctx) + handler.run(kwargs, progress=progress) + + +class ImportHarvest(ImportSubcommand): """ Import data to Rattail, from Harvest API """ diff --git a/setup.cfg b/setup.cfg index 0e27e0b..72936e7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,6 +35,9 @@ include_package_data = True rattail.subcommands = import-harvest = rattail_harvest.commands:ImportHarvest +rattail.typer_imports = + rattail_harvest = rattail_harvest.commands + rattail.config.extensions = rattail_harvest = rattail_harvest.config:RattailHarvestExtension From 02298a7a2989e3eb89e499dadf2ba3aa037250a6 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 6 Jun 2024 18:16:17 -0500 Subject: [PATCH 31/42] Update changelog --- CHANGELOG.md | 4 ++++ rattail_harvest/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 092b760..62ab236 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to rattail-harvest 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). +## [0.2.0] - 2024-06-06 +### Changed +- Add typer equivalents for `rattail` commands. + ## [0.1.2] - 2023-11-18 ### Changed - Catch-up release, with various schema changes etc. diff --git a/rattail_harvest/_version.py b/rattail_harvest/_version.py index de58c20..08b390b 100644 --- a/rattail_harvest/_version.py +++ b/rattail_harvest/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.2' +__version__ = '0.2.0' From 1cfd30a21178525a5ac21312ebde035a9b2ad73f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 6 Jun 2024 18:17:26 -0500 Subject: [PATCH 32/42] Fix default dist filename for release task not sure why this fix was needed, did setuptools behavior change? --- tasks.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tasks.py b/tasks.py index 6454e3f..d411697 100644 --- a/tasks.py +++ b/tasks.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2024 Lance Edgar # # This file is part of Rattail. # @@ -35,14 +35,14 @@ exec(open(os.path.join(here, 'rattail_harvest', '_version.py')).read()) @task -def release(ctx): +def release(c): """ Release a new version of rattail-harvest """ # rebuild local tar.gz file for distribution shutil.rmtree('rattail_harvest.egg-info') - ctx.run('python setup.py sdist --formats=gztar') + c.run('python setup.py sdist --formats=gztar') # upload to public PyPI - filename = 'rattail-harvest-{}.tar.gz'.format(__version__) - ctx.run('twine upload dist/{}'.format(filename)) + filename = f'rattail_harvest-{__version__}.tar.gz' + c.run(f'twine upload dist/{filename}') From 5ae7db78a51cf80511975ebce6d9972d5a35b2b4 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 6 Jun 2024 18:53:15 -0500 Subject: [PATCH 33/42] Add alembic scripts to project manifest --- MANIFEST.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index cf92731..d765c69 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,5 @@ include *.md include *.rst + recursive-include rattail_harvest/db/alembic *.mako +recursive-include rattail_harvest/db/alembic *.py From 70f496893b74648257f3750180b4c9ba445998ab Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 6 Jun 2024 18:53:43 -0500 Subject: [PATCH 34/42] Update changelog --- CHANGELOG.md | 4 ++++ rattail_harvest/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62ab236..7ee2579 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to rattail-harvest 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). +## [0.2.1] - 2024-06-06 +### Changed +- Add alembic scripts to project manifest. + ## [0.2.0] - 2024-06-06 ### Changed - Add typer equivalents for `rattail` commands. diff --git a/rattail_harvest/_version.py b/rattail_harvest/_version.py index 08b390b..53d3efe 100644 --- a/rattail_harvest/_version.py +++ b/rattail_harvest/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.2.0' +__version__ = '0.2.1' From ffe33b20bc2836d2b708c1fd8cde17d29c73f8d3 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 10 Jun 2024 19:31:21 -0500 Subject: [PATCH 35/42] feat: switch from setup.cfg to pyproject.toml + hatchling --- .gitignore | 3 ++ pyproject.toml | 57 +++++++++++++++++++++++++++++++++++++ rattail_harvest/_version.py | 5 +++- setup.cfg | 45 ----------------------------- setup.py | 29 ------------------- tasks.py | 18 ++++++++++-- 6 files changed, 79 insertions(+), 78 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 21277ed..129bcb9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ +*~ +*.pyc +dist/ rattail_harvest.egg-info/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..66149a9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,57 @@ + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + + +[project] +name = "rattail-harvest" +version = "0.2.1" +description = "Rattail integration package for Harvest" +readme = "README.rst" +authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] +license = {text = "GNU GPL v3+"} +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "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 = [ + "invoke", + "rattail[db]", +] + + +[project.urls] +Homepage = "https://rattailproject.org" +Repository = "https://kallithea.rattailproject.org/rattail-project/rattail-harvest" +Changelog = "https://kallithea.rattailproject.org/rattail-project/rattail-harvest/files/master/CHANGELOG.md" + + +[project.entry-points."rattail.subcommands"] +import-harvest = "rattail_harvest.commands:ImportHarvest" + + +[project.entry-points."rattail.typer_imports"] +rattail_harvest = "rattail_harvest.commands" + + +[project.entry-points."rattail.config.extensions"] +rattail_harvest = "rattail_harvest.config:RattailHarvestExtension" + + +[project.entry-points."rattail.importing"] +"to_rattail.from_harvest.import" = "rattail_harvest.importing.harvest:FromHarvestToRattail" + + +[tool.commitizen] +version_provider = "pep621" +tag_format = "v$version" +update_changelog_on_bump = true diff --git a/rattail_harvest/_version.py b/rattail_harvest/_version.py index 53d3efe..885cd51 100644 --- a/rattail_harvest/_version.py +++ b/rattail_harvest/_version.py @@ -1,3 +1,6 @@ # -*- coding: utf-8; -*- -__version__ = '0.2.1' +from importlib.metadata import version + + +__version__ = version('rattail-harvest') diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 72936e7..0000000 --- a/setup.cfg +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8; -*- - -[metadata] -name = rattail-harvest -version = attr: rattail_harvest.__version__ -author = Lance Edgar -author_email = lance@edbob.org -url = https://rattailproject.org/ -description = Rattail integration package for Harvest -long_description = file: README.rst -classifiers = - Development Status :: 3 - Alpha - Environment :: Console - 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 - - -[options] -install_requires = - invoke - rattail[db] - -packages = find: -include_package_data = True - - -[options.entry_points] - -rattail.subcommands = - import-harvest = rattail_harvest.commands:ImportHarvest - -rattail.typer_imports = - rattail_harvest = rattail_harvest.commands - -rattail.config.extensions = - rattail_harvest = rattail_harvest.config:RattailHarvestExtension - -rattail.importing = - to_rattail.from_harvest.import = rattail_harvest.importing.harvest:FromHarvestToRattail diff --git a/setup.py b/setup.py deleted file mode 100644 index 71a5a0a..0000000 --- a/setup.py +++ /dev/null @@ -1,29 +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 . -# -################################################################################ -""" -rattail-harvest setup script -""" - -from setuptools import setup - -setup() diff --git a/tasks.py b/tasks.py index d411697..3ed6102 100644 --- a/tasks.py +++ b/tasks.py @@ -25,13 +25,24 @@ Tasks for rattail-harvest """ import os +import re import shutil from invoke import task here = os.path.abspath(os.path.dirname(__file__)) -exec(open(os.path.join(here, 'rattail_harvest', '_version.py')).read()) +__version__ = None +pattern = re.compile(r'^version = "(\d+\.\d+\.\d+)"$') +with open(os.path.join(here, 'pyproject.toml'), 'rt') as f: + for line in f: + line = line.rstrip('\n') + match = pattern.match(line) + if match: + __version__ = match.group(1) + break +if not __version__: + raise RuntimeError("could not parse version!") @task @@ -40,8 +51,9 @@ def release(c): Release a new version of rattail-harvest """ # rebuild local tar.gz file for distribution - shutil.rmtree('rattail_harvest.egg-info') - c.run('python setup.py sdist --formats=gztar') + if os.path.exists('rattail_harvest.egg-info'): + shutil.rmtree('rattail_harvest.egg-info') + c.run('python -m build --sdist') # upload to public PyPI filename = f'rattail_harvest-{__version__}.tar.gz' From d419bc18544d7bb3f850948328ac5543453a3183 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 10 Jun 2024 19:31:34 -0500 Subject: [PATCH 36/42] =?UTF-8?q?bump:=20version=200.2.1=20=E2=86=92=200.3?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ee2579..626eb8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to rattail-harvest 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.0 (2024-06-10) + +### Feat + +- switch from setup.cfg to pyproject.toml + hatchling + ## [0.2.1] - 2024-06-06 ### Changed - Add alembic scripts to project manifest. diff --git a/pyproject.toml b/pyproject.toml index 66149a9..ab41dfa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "rattail-harvest" -version = "0.2.1" +version = "0.3.0" description = "Rattail integration package for Harvest" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From ff202415e330a2d26f3797f556b29ff57391bc44 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 1 Jul 2024 12:32:20 -0500 Subject: [PATCH 37/42] fix: remove legacy command definitions --- pyproject.toml | 4 ---- rattail_harvest/commands.py | 11 +---------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ab41dfa..3d93740 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,10 +35,6 @@ Repository = "https://kallithea.rattailproject.org/rattail-project/rattail-harve Changelog = "https://kallithea.rattailproject.org/rattail-project/rattail-harvest/files/master/CHANGELOG.md" -[project.entry-points."rattail.subcommands"] -import-harvest = "rattail_harvest.commands:ImportHarvest" - - [project.entry-points."rattail.typer_imports"] rattail_harvest = "rattail_harvest.commands" diff --git a/rattail_harvest/commands.py b/rattail_harvest/commands.py index 7cc804b..c54848c 100644 --- a/rattail_harvest/commands.py +++ b/rattail_harvest/commands.py @@ -26,7 +26,7 @@ rattail-harvest commands import typer -from rattail.commands import rattail_typer, ImportSubcommand +from rattail.commands import rattail_typer from rattail.commands.typer import importer_command, typer_get_runas_user from rattail.commands.importing import ImportCommandHandler @@ -46,12 +46,3 @@ def import_harvest( config, import_handler_key='to_rattail.from_harvest.import') kwargs['user'] = typer_get_runas_user(ctx) handler.run(kwargs, progress=progress) - - -class ImportHarvest(ImportSubcommand): - """ - Import data to Rattail, from Harvest API - """ - name = 'import-harvest' - description = __doc__.strip() - handler_key = 'to_rattail.from_harvest.import' From 0521f5e38cfae6266223c61c50537eb5187811fc Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 1 Jul 2024 14:00:10 -0500 Subject: [PATCH 38/42] =?UTF-8?q?bump:=20version=200.3.0=20=E2=86=92=200.3?= =?UTF-8?q?.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 626eb8f..d35b2c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to rattail-harvest 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.1 (2024-07-01) + +### Fix + +- remove legacy command definitions + ## v0.3.0 (2024-06-10) ### Feat diff --git a/pyproject.toml b/pyproject.toml index 3d93740..59a22d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "rattail-harvest" -version = "0.3.0" +version = "0.3.1" description = "Rattail integration package for Harvest" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 9687c8552954b864141d6c522d3689f1dd93dfe9 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 16 Aug 2024 13:32:56 -0500 Subject: [PATCH 39/42] fix: avoid deprecated base class for config extension --- rattail_harvest/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rattail_harvest/config.py b/rattail_harvest/config.py index 9fedb3e..93cc4d1 100644 --- a/rattail_harvest/config.py +++ b/rattail_harvest/config.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2024 Lance Edgar # # This file is part of Rattail. # @@ -24,10 +24,10 @@ Config Extension """ -from rattail.config import ConfigExtension +from wuttjamaican.conf import WuttaConfigExtension -class RattailHarvestExtension(ConfigExtension): +class RattailHarvestExtension(WuttaConfigExtension): """ Config extension for rattail-harvest. """ From df324159ba8809f8a19c3a860d8f26d5faa5f16b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 18 Aug 2024 20:09:43 -0500 Subject: [PATCH 40/42] =?UTF-8?q?bump:=20version=200.3.1=20=E2=86=92=200.3?= =?UTF-8?q?.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d35b2c5..c1c7891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to rattail-harvest 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.2 (2024-08-18) + +### Fix + +- avoid deprecated base class for config extension + ## v0.3.1 (2024-07-01) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 59a22d4..0880bb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "rattail-harvest" -version = "0.3.1" +version = "0.3.2" description = "Rattail integration package for Harvest" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 69ce07a778efa44bcf937f14498ed70b466bbd8e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 13 Sep 2024 18:38:44 -0500 Subject: [PATCH 41/42] docs: use markdown for readme file --- README.md | 11 +++++++++++ README.rst | 14 -------------- pyproject.toml | 2 +- 3 files changed, 12 insertions(+), 15 deletions(-) create mode 100644 README.md delete mode 100644 README.rst diff --git a/README.md b/README.md new file mode 100644 index 0000000..3a6e08e --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ + +# rattail-harvest + +Rattail is a retail software framework, released under the GNU General +Public License. + +This package contains software interfaces for +[Harvest](https://www.getharvest.com/). + +Please see the [Rattail Project](https://rattailproject.org/) for more +information. diff --git a/README.rst b/README.rst deleted file mode 100644 index ab02203..0000000 --- a/README.rst +++ /dev/null @@ -1,14 +0,0 @@ - -rattail-harvest -=============== - -Rattail is a retail software framework, released under the GNU General -Public License. - -This package contains software interfaces for `Harvest`_. - -.. _`Harvest`: https://www.getharvest.com/ - -Please see the `Rattail Project`_ for more information. - -.. _`Rattail Project`: https://rattailproject.org/ diff --git a/pyproject.toml b/pyproject.toml index 0880bb8..c862f63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "hatchling.build" name = "rattail-harvest" version = "0.3.2" description = "Rattail integration package for Harvest" -readme = "README.rst" +readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] license = {text = "GNU GPL v3+"} classifiers = [ From e0b8797046c5d679bcac5c91b679d0580053883e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 14 Sep 2024 13:20:04 -0500 Subject: [PATCH 42/42] docs: update project links, kallithea -> forgejo --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c862f63..8002dd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,8 +31,8 @@ dependencies = [ [project.urls] Homepage = "https://rattailproject.org" -Repository = "https://kallithea.rattailproject.org/rattail-project/rattail-harvest" -Changelog = "https://kallithea.rattailproject.org/rattail-project/rattail-harvest/files/master/CHANGELOG.md" +Repository = "https://forgejo.wuttaproject.org/rattail/rattail-harvest" +Changelog = "https://forgejo.wuttaproject.org/rattail/rattail-harvest/src/branch/master/CHANGELOG.md" [project.entry-points."rattail.typer_imports"]