diff --git a/.gitignore b/.gitignore index 129bcb9..21277ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1 @@ -*~ -*.pyc -dist/ rattail_harvest.egg-info/ diff --git a/CHANGELOG.md b/CHANGELOG.md index c1c7891..0cc2c69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,36 +5,6 @@ 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 - -- remove legacy command definitions - -## 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. - -## [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. - ## [0.1.1] - 2022-01-29 ### Added - Initial version. diff --git a/MANIFEST.in b/MANIFEST.in index d765c69..cf92731 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,3 @@ include *.md include *.rst - recursive-include rattail_harvest/db/alembic *.mako -recursive-include rattail_harvest/db/alembic *.py diff --git a/README.md b/README.md deleted file mode 100644 index 3a6e08e..0000000 --- a/README.md +++ /dev/null @@ -1,11 +0,0 @@ - -# 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 new file mode 100644 index 0000000..ab02203 --- /dev/null +++ b/README.rst @@ -0,0 +1,14 @@ + +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 deleted file mode 100644 index 8002dd2..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,53 +0,0 @@ - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - - -[project] -name = "rattail-harvest" -version = "0.3.2" -description = "Rattail integration package for Harvest" -readme = "README.md" -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://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"] -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 885cd51..4984097 100644 --- a/rattail_harvest/_version.py +++ b/rattail_harvest/_version.py @@ -1,6 +1,3 @@ # -*- coding: utf-8; -*- -from importlib.metadata import version - - -__version__ = version('rattail-harvest') +__version__ = '0.1.1' diff --git a/rattail_harvest/commands.py b/rattail_harvest/commands.py index c54848c..80d5be1 100644 --- a/rattail_harvest/commands.py +++ b/rattail_harvest/commands.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2024 Lance Edgar +# Copyright © 2010-2022 Lance Edgar # # This file is part of Rattail. # @@ -24,25 +24,13 @@ rattail-harvest commands """ -import typer - -from rattail.commands import rattail_typer -from rattail.commands.typer import importer_command, typer_get_runas_user -from rattail.commands.importing import ImportCommandHandler +from rattail import commands -@rattail_typer.command() -@importer_command -def import_harvest( - ctx: typer.Context, - **kwargs -): +class ImportHarvest(commands.ImportSubcommand): """ 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) + name = 'import-harvest' + description = __doc__.strip() + handler_key = 'to_rattail.from_harvest.import' diff --git a/rattail_harvest/config.py b/rattail_harvest/config.py deleted file mode 100644 index 93cc4d1..0000000 --- a/rattail_harvest/config.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# Rattail -- Retail Software Framework -# Copyright © 2010-2024 Lance Edgar -# -# This file is part of Rattail. -# -# Rattail is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# Rattail. If not, see . -# -################################################################################ -""" -Config Extension -""" - -from wuttjamaican.conf import WuttaConfigExtension - - -class RattailHarvestExtension(WuttaConfigExtension): - """ - 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/rattail_harvest/db/alembic/versions/53c066772ad5_rename_cache_tables.py b/rattail_harvest/db/alembic/versions/53c066772ad5_rename_cache_tables.py deleted file mode 100644 index b185837..0000000 --- a/rattail_harvest/db/alembic/versions/53c066772ad5_rename_cache_tables.py +++ /dev/null @@ -1,179 +0,0 @@ -# -*- 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/alembic/versions/5505c0e60d28_add_project_deleted.py b/rattail_harvest/db/alembic/versions/5505c0e60d28_add_project_deleted.py deleted file mode 100644 index 042663d..0000000 --- a/rattail_harvest/db/alembic/versions/5505c0e60d28_add_project_deleted.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- 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/alembic/versions/6bc1cb21d920_add_harvest_user_person.py b/rattail_harvest/db/alembic/versions/6bc1cb21d920_add_harvest_user_person.py deleted file mode 100644 index 31cca85..0000000 --- a/rattail_harvest/db/alembic/versions/6bc1cb21d920_add_harvest_user_person.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- 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/alembic/versions/a1cf300fb371_fix_indeces.py b/rattail_harvest/db/alembic/versions/a1cf300fb371_fix_indeces.py deleted file mode 100644 index fe059e0..0000000 --- a/rattail_harvest/db/alembic/versions/a1cf300fb371_fix_indeces.py +++ /dev/null @@ -1,105 +0,0 @@ -# -*- 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) diff --git a/rattail_harvest/db/alembic/versions/f2a1650e7fbc_grow_id_fields.py b/rattail_harvest/db/alembic/versions/f2a1650e7fbc_grow_id_fields.py deleted file mode 100644 index 0c24bb5..0000000 --- a/rattail_harvest/db/alembic/versions/f2a1650e7fbc_grow_id_fields.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- 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/__init__.py b/rattail_harvest/db/model/__init__.py index 86a099f..e55b2a1 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-2023 Lance Edgar +# Copyright © 2010-2022 Lance Edgar # # This file is part of Rattail. # @@ -24,6 +24,5 @@ Harvest integration data models """ -from .harvest import (HarvestCacheUser, HarvestCacheClient, - HarvestCacheProject, HarvestCacheTask, - HarvestCacheTimeEntry) +from .harvest import (HarvestUser, HarvestClient, HarvestProject, + HarvestTask, HarvestTimeEntry) diff --git a/rattail_harvest/db/model/harvest.py b/rattail_harvest/db/model/harvest.py index 4b015d2..d7a098c 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-2023 Lance Edgar +# Copyright © 2010-2022 Lance Edgar # # This file is part of Rattail. # @@ -24,8 +24,6 @@ Harvest "cache" data models """ -import warnings - import sqlalchemy as sa from sqlalchemy import orm @@ -33,23 +31,21 @@ from rattail.db import model from rattail.db.util import normalize_full_name -class HarvestCacheUser(model.Base): +class HarvestUser(model.Base): """ Represents a user record in Harvest. https://help.getharvest.com/api-v2/users-api/users/users/#the-user-object """ - __tablename__ = 'harvest_cache_user' + __tablename__ = 'harvest_user' __table_args__ = ( - sa.ForeignKeyConstraint(['person_uuid'], ['person.uuid'], - name='harvest_cache_user_fk_person'), - sa.UniqueConstraint('id', name='harvest_cache_user_uq_id'), + sa.UniqueConstraint('id', name='harvest_user_uq_id'), ) __versioned__ = {} uuid = model.uuid_column() - id = sa.Column(sa.BigInteger(), nullable=False) + id = sa.Column(sa.Integer(), nullable=False) first_name = sa.Column(sa.String(length=255), nullable=True) @@ -94,38 +90,25 @@ class HarvestCacheUser(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) -class HarvestCacheClient(model.Base): +class HarvestClient(model.Base): """ Represents a client record in Harvest. https://help.getharvest.com/api-v2/clients-api/clients/clients/#the-client-object """ - __tablename__ = 'harvest_cache_client' + __tablename__ = 'harvest_client' __table_args__ = ( - sa.UniqueConstraint('id', name='harvest_cache_client_uq_id'), + sa.UniqueConstraint('id', name='harvest_client_uq_id'), ) __versioned__ = {} uuid = model.uuid_column() - id = sa.Column(sa.BigInteger(), nullable=False) + id = sa.Column(sa.Integer(), nullable=False) name = sa.Column(sa.String(length=255), nullable=True) @@ -143,26 +126,25 @@ class HarvestCacheClient(model.Base): return self.name or '' -class HarvestCacheProject(model.Base): +class HarvestProject(model.Base): """ Represents a project record in Harvest. https://help.getharvest.com/api-v2/projects-api/projects/projects/#the-project-object """ - __tablename__ = 'harvest_cache_project' + __tablename__ = 'harvest_project' __table_args__ = ( - sa.UniqueConstraint('id', name='harvest_cache_project_uq_id'), - sa.ForeignKeyConstraint(['client_id'], ['harvest_cache_client.id'], - name='harvest_cache_project_fk_client'), + sa.UniqueConstraint('id', name='harvest_project_uq_id'), + sa.ForeignKeyConstraint(['client_id'], ['harvest_client.id'], name='harvest_project_fk_client'), ) __versioned__ = {'exclude': ['over_budget_notification_date']} uuid = model.uuid_column() - id = sa.Column(sa.BigInteger(), nullable=False) + id = sa.Column(sa.Integer(), nullable=False) - client_id = sa.Column(sa.BigInteger(), nullable=True) # TODO: should not allow null? - client = orm.relationship(HarvestCacheClient, backref=orm.backref('projects')) + client_id = sa.Column(sa.Integer(), nullable=True) # TODO: should not allow null? + client = orm.relationship(HarvestClient, backref=orm.backref('projects')) name = sa.Column(sa.String(length=255), nullable=True) @@ -208,29 +190,25 @@ class HarvestCacheProject(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 '' -class HarvestCacheTask(model.Base): +class HarvestTask(model.Base): """ Represents a task record in Harvest. https://help.getharvest.com/api-v2/tasks-api/tasks/tasks/#the-task-object """ - __tablename__ = 'harvest_cache_task' + __tablename__ = 'harvest_task' __table_args__ = ( - sa.UniqueConstraint('id', name='harvest_cache_task_uq_id'), + sa.UniqueConstraint('id', name='harvest_task_uq_id'), ) __versioned__ = {} uuid = model.uuid_column() - id = sa.Column(sa.BigInteger(), nullable=False) + id = sa.Column(sa.Integer(), nullable=False) name = sa.Column(sa.String(length=255), nullable=True) @@ -250,46 +228,42 @@ class HarvestCacheTask(model.Base): return self.name or '' -class HarvestCacheTimeEntry(model.Base): +class HarvestTimeEntry(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_cache_time_entry' + __tablename__ = 'harvest_time_entry' __table_args__ = ( - 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'), + 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'), ) __versioned__ = {} model_title_plural = "Harvest Time Entries" uuid = model.uuid_column() - id = sa.Column(sa.BigInteger(), nullable=False) + id = sa.Column(sa.Integer(), nullable=False) spent_date = sa.Column(sa.Date(), nullable=True) - user_id = sa.Column(sa.BigInteger(), nullable=True) - user = orm.relationship(HarvestCacheUser, backref=orm.backref('time_entries')) + user_id = sa.Column(sa.Integer(), nullable=True) + user = orm.relationship(HarvestUser, backref=orm.backref('time_entries')) - client_id = sa.Column(sa.BigInteger(), nullable=True) - client = orm.relationship(HarvestCacheClient, backref=orm.backref('time_entries')) + client_id = sa.Column(sa.Integer(), nullable=True) + client = orm.relationship(HarvestClient, backref=orm.backref('time_entries')) - project_id = sa.Column(sa.BigInteger(), nullable=True) - project = orm.relationship(HarvestCacheProject, backref=orm.backref('time_entries')) + project_id = sa.Column(sa.Integer(), nullable=True) + project = orm.relationship(HarvestProject, backref=orm.backref('time_entries')) - task_id = sa.Column(sa.BigInteger(), nullable=True) - task = orm.relationship(HarvestCacheTask, backref=orm.backref('time_entries')) + task_id = sa.Column(sa.Integer(), nullable=True) + task = orm.relationship(HarvestTask, backref=orm.backref('time_entries')) - invoice_id = sa.Column(sa.BigInteger(), nullable=True) + invoice_id = sa.Column(sa.Integer(), nullable=True) hours = sa.Column(sa.Numeric(precision=6, scale=2), nullable=True) diff --git a/rattail_harvest/harvest/config.py b/rattail_harvest/harvest/config.py deleted file mode 100644 index 3a2cbd6..0000000 --- a/rattail_harvest/harvest/config.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- 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('/') diff --git a/rattail_harvest/harvest/importing/__init__.py b/rattail_harvest/harvest/importing/__init__.py deleted file mode 100644 index e0b3e09..0000000 --- a/rattail_harvest/harvest/importing/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- 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 deleted file mode 100644 index eacbeb0..0000000 --- a/rattail_harvest/harvest/importing/model.py +++ /dev/null @@ -1,169 +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 . -# -################################################################################ -""" -Harvest model importers -""" - -from rattail import importing -from rattail_harvest.harvest.webapi import make_harvest_webapi - - -class ToHarvest(importing.Importer): - - def setup(self): - 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) - - -class TimeEntryImporter(ToHarvest): - """ - Harvest time entry data importer. - """ - model_name = 'TimeEntry' - key = 'id' - supported_fields = [ - 'id', - 'user_id', - 'client_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 = {} - - # TODO: we try to avoid entries w/ timer still running here, - # but for some reason they still come back, so double-check - 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']: - 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 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'], - 'client_id': entry['client']['id'], - '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 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 - 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'], - # '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, entry, host_data, local_data=None, all_fields=False): - if self.dry_run: - return entry - - 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'], - } - - return self.webapi.update_time_entry(entry['id'], **kwargs) - - def delete_object(self, entry): - if self.dry_run: - return True - - self.webapi.delete_time_entry(entry['id']) - return True diff --git a/rattail_harvest/harvest/webapi.py b/rattail_harvest/harvest/webapi.py index fafdcb8..6107b19 100644 --- a/rattail_harvest/harvest/webapi.py +++ b/rattail_harvest/harvest/webapi.py @@ -58,12 +58,6 @@ 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) - 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)) @@ -82,18 +76,6 @@ 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 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. @@ -131,17 +113,7 @@ class HarvestWebAPI(object): https://help.getharvest.com/api-v2/projects-api/projects/projects/#list-all-projects """ response = self.get('/projects', params=kwargs) - 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 + return response.json() def get_tasks(self, **kwargs): """ @@ -179,15 +151,10 @@ class HarvestWebAPI(object): https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/#retrieve-a-time-entry """ - 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() + response = self.get('/time_entries/{}'.format(time_entry_id)) + return response.json() - def create_time_entry(self, **kwargs): + def put_time_entry(self, **kwargs): """ Create a new time entry. All kwargs are passed on as POST parameters. @@ -200,41 +167,3 @@ class HarvestWebAPI(object): raise ValueError("must provide all of: {}".format(', '.join(required))) 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. - - 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 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 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') - 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 4f22ef1..8cbd9dd 100644 --- a/rattail_harvest/importing/harvest.py +++ b/rattail_harvest/importing/harvest.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2023 Lance Edgar +# Copyright © 2010-2022 Lance Edgar # # This file is part of Rattail. # @@ -27,13 +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 rattail_harvest.harvest.webapi import HarvestWebAPI log = logging.getLogger(__name__) @@ -49,11 +47,11 @@ class FromHarvestToRattail(importing.ToRattailHandler): def get_importers(self): importers = OrderedDict() - importers['HarvestCacheUser'] = HarvestCacheUserImporter - importers['HarvestCacheClient'] = HarvestCacheClientImporter - importers['HarvestCacheProject'] = HarvestCacheProjectImporter - importers['HarvestCacheTask'] = HarvestCacheTaskImporter - importers['HarvestCacheTimeEntry'] = HarvestCacheTimeEntryImporter + importers['HarvestUser'] = HarvestUserImporter + importers['HarvestClient'] = HarvestClientImporter + importers['HarvestProject'] = HarvestProjectImporter + importers['HarvestTask'] = HarvestTaskImporter + importers['HarvestTimeEntry'] = HarvestTimeEntryImporter return importers @@ -71,7 +69,13 @@ class FromHarvest(importing.Importer): def setup(self): super(FromHarvest, self).setup() - self.webapi = make_harvest_webapi(self.config) + + 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) def time_from_harvest(self, value): # all harvest times appear to come as UTC, so no conversion needed @@ -90,17 +94,14 @@ class FromHarvest(importing.Importer): return data -class HarvestCacheUserImporter(FromHarvest, rattail_harvest_importing.model.HarvestCacheUserImporter): +class HarvestUserImporter(FromHarvest, rattail_harvest_importing.model.HarvestUserImporter): """ Import user data from Harvest """ @property def supported_fields(self): - fields = list(super().supported_fields) - - # this is for local tracking only; is not in harvest - fields.remove('person_uuid') + fields = list(super(HarvestUserImporter, self).supported_fields) # this used to be in harvest i thought, but is no longer? fields.remove('name') @@ -110,28 +111,8 @@ class HarvestCacheUserImporter(FromHarvest, rattail_harvest_importing.model.Harv def get_host_objects(self): return self.webapi.get_users()['users'] - def normalize_host_object(self, user): - data = super().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 - - return data - - -class HarvestCacheClientImporter(FromHarvest, rattail_harvest_importing.model.HarvestCacheClientImporter): +class HarvestClientImporter(FromHarvest, rattail_harvest_importing.model.HarvestClientImporter): """ Import client data from Harvest """ @@ -140,32 +121,16 @@ class HarvestCacheClientImporter(FromHarvest, rattail_harvest_importing.model.Ha return self.webapi.get_clients()['clients'] -class HarvestCacheProjectImporter(FromHarvest, rattail_harvest_importing.model.HarvestCacheProjectImporter): +class HarvestProjectImporter(FromHarvest, rattail_harvest_importing.model.HarvestProjectImporter): """ Import project data from Harvest """ - @property - def supported_fields(self): - fields = list(super().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.HarvestCacheProject)\ - .filter(sa.or_( - model.HarvestCacheProject.deleted == False, - model.HarvestCacheProject.deleted == None)) - def get_host_objects(self): - return self.webapi.get_projects() + return self.webapi.get_projects()['projects'] def normalize_host_object(self, project): - data = super().normalize_host_object(project) + data = super(HarvestProjectImporter, self).normalize_host_object(project) if not data: return @@ -203,15 +168,8 @@ class HarvestCacheProjectImporter(FromHarvest, rattail_harvest_importing.model.H return data - def can_delete_object(self, project, data): - return not project.deleted - def delete_object(self, project): - project.deleted = True - return True - - -class HarvestCacheTaskImporter(FromHarvest, rattail_harvest_importing.model.HarvestCacheTaskImporter): +class HarvestTaskImporter(FromHarvest, rattail_harvest_importing.model.HarvestTaskImporter): """ Import task data from Harvest """ @@ -220,49 +178,40 @@ class HarvestCacheTaskImporter(FromHarvest, rattail_harvest_importing.model.Harv return self.webapi.get_tasks()['tasks'] -class HarvestCacheTimeEntryImporter(FromHarvest, rattail_harvest_importing.model.HarvestCacheTimeEntryImporter): +class HarvestTimeEntryImporter(FromHarvest, rattail_harvest_importing.model.HarvestTimeEntryImporter): """ Import time entry data from Harvest """ - def get_host_objects(self): - 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 setup(self): + super(HarvestTimeEntryImporter, self).setup() + model = self.model - 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) + 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}) def normalize_host_object(self, entry): - data = super().normalize_host_object(entry) + data = super(HarvestTimeEntryImporter, self).normalize_host_object(entry) if not data: return - if entry['is_running']: - log.debug("Harvest time entry is still running: %s", entry) - return - 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 5e35839..6fbcfdb 100644 --- a/rattail_harvest/importing/model.py +++ b/rattail_harvest/importing/model.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2023 Lance Edgar +# Copyright © 2010-2022 Lance Edgar # # This file is part of Rattail. # @@ -24,86 +24,30 @@ 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 ############################## -class HarvestCacheUserImporter(ToRattail): - model_class = model.HarvestCacheUser +class HarvestUserImporter(ToRattail): + model_class = model.HarvestUser -class HarvestCacheClientImporter(ToRattail): - model_class = model.HarvestCacheClient +class HarvestClientImporter(ToRattail): + model_class = model.HarvestClient -class HarvestCacheProjectImporter(ToRattail): - model_class = model.HarvestCacheProject +class HarvestProjectImporter(ToRattail): + model_class = model.HarvestProject -class HarvestCacheTaskImporter(ToRattail): - model_class = model.HarvestCacheTask +class HarvestTaskImporter(ToRattail): + model_class = model.HarvestTask -class HarvestCacheTimeEntryImporter(ToRattail): - model_class = model.HarvestCacheTimeEntry - - # 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().setup() - model = self.model - - if 'project_id' in self.fields: - self.harvest_projects_by_id = self.app.cache_model( - self.session, model.HarvestCacheProject, key='id') +class HarvestTimeEntryImporter(ToRattail): + model_class = model.HarvestTimeEntry def cache_query(self): - 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'): - return self.harvest_projects_by_id.get(project_id) - - model = self.model - return self.session.query(model.HarvestCacheProject)\ - .filter(model.HarvestCacheProject.id == project_id)\ - .first() - - def update_object(self, entry, data, local_data=None): - entry = super().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.HarvestCacheProject() - 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 + 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) diff --git a/rattail_harvest/importing/rattail.py b/rattail_harvest/importing/rattail.py index ab67489..da1e9c6 100644 --- a/rattail_harvest/importing/rattail.py +++ b/rattail_harvest/importing/rattail.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2023 Lance Edgar +# Copyright © 2010-2022 Lance Edgar # # This file is part of Rattail. # @@ -34,11 +34,11 @@ class FromRattailToRattailHarvestMixin(object): """ def add_harvest_importers(self, importers): - importers['HarvestCacheUser'] = HarvestCacheUserImporter - importers['HarvestCacheClient'] = HarvestCacheClientImporter - importers['HarvestCacheProject'] = HarvestCacheProjectImporter - importers['HarvestCacheTask'] = HarvestCacheTaskImporter - importers['HarvestCacheTimeEntry'] = HarvestCacheTimeEntryImporter + importers['HarvestUser'] = HarvestUserImporter + importers['HarvestClient'] = HarvestClientImporter + importers['HarvestProject'] = HarvestProjectImporter + importers['HarvestTask'] = HarvestTaskImporter + importers['HarvestTimeEntry'] = HarvestTimeEntryImporter return importers @@ -46,26 +46,17 @@ class FromRattailToRattailHarvestMixin(object): # harvest cache models ############################## -class HarvestCacheUserImporter(base.FromRattail, rattail_harvest_importing.model.HarvestCacheUserImporter): +class HarvestUserImporter(base.FromRattail, rattail_harvest_importing.model.HarvestUserImporter): pass -class HarvestCacheClientImporter(base.FromRattail, rattail_harvest_importing.model.HarvestCacheClientImporter): +class HarvestClientImporter(base.FromRattail, rattail_harvest_importing.model.HarvestClientImporter): pass -class HarvestCacheProjectImporter(base.FromRattail, rattail_harvest_importing.model.HarvestCacheProjectImporter): +class HarvestProjectImporter(base.FromRattail, rattail_harvest_importing.model.HarvestProjectImporter): pass -class HarvestCacheTaskImporter(base.FromRattail, rattail_harvest_importing.model.HarvestCacheTaskImporter): +class HarvestTaskImporter(base.FromRattail, rattail_harvest_importing.model.HarvestTaskImporter): pass -class HarvestCacheTimeEntryImporter(base.FromRattail, rattail_harvest_importing.model.HarvestCacheTimeEntryImporter): - - def query(self): - 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 +class HarvestTimeEntryImporter(base.FromRattail, rattail_harvest_importing.model.HarvestTimeEntryImporter): + pass diff --git a/rattail_harvest/importing/versions.py b/rattail_harvest/importing/versions.py deleted file mode 100644 index e82d861..0000000 --- a/rattail_harvest/importing/versions.py +++ /dev/null @@ -1,73 +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 -> Rattail "versions" data import -""" - -from rattail.importing import versions as base - - -class HarvestVersionMixin(object): - - def add_harvest_importers(self, importers): - importers['HarvestCacheUser'] = HarvestCacheUserImporter - importers['HarvestCacheClient'] = HarvestCacheClientImporter - importers['HarvestCacheProject'] = HarvestCacheProjectImporter - importers['HarvestCacheTask'] = HarvestCacheTaskImporter - importers['HarvestCacheTimeEntry'] = HarvestCacheTimeEntryImporter - return importers - - -class HarvestCacheUserImporter(base.VersionImporter): - - @property - def host_model_class(self): - return self.model.HarvestCacheUser - - -class HarvestCacheClientImporter(base.VersionImporter): - - @property - def host_model_class(self): - return self.model.HarvestCacheClient - - -class HarvestCacheProjectImporter(base.VersionImporter): - - @property - def host_model_class(self): - return self.model.HarvestCacheProject - - -class HarvestCacheTaskImporter(base.VersionImporter): - - @property - def host_model_class(self): - return self.model.HarvestCacheTask - - -class HarvestCacheTimeEntryImporter(base.VersionImporter): - - @property - def host_model_class(self): - return self.model.HarvestCacheTimeEntry diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c11e7a1 --- /dev/null +++ b/setup.py @@ -0,0 +1,106 @@ +# -*- 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-harvest setup script +""" + +import os +from setuptools import setup, find_packages + + +here = os.path.abspath(os.path.dirname(__file__)) +exec(open(os.path.join(here, 'rattail_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.importing': [ + 'to_rattail.from_harvest.import = rattail_harvest.importing.harvest:FromHarvestToRattail', + ], + }, +) diff --git a/tasks.py b/tasks.py index 3ed6102..6454e3f 100644 --- a/tasks.py +++ b/tasks.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2024 Lance Edgar +# Copyright © 2010-2022 Lance Edgar # # This file is part of Rattail. # @@ -25,36 +25,24 @@ Tasks for rattail-harvest """ import os -import re import shutil from invoke import task here = os.path.abspath(os.path.dirname(__file__)) -__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!") +exec(open(os.path.join(here, 'rattail_harvest', '_version.py')).read()) @task -def release(c): +def release(ctx): """ Release a new version of rattail-harvest """ # rebuild local tar.gz file for distribution - if os.path.exists('rattail_harvest.egg-info'): - shutil.rmtree('rattail_harvest.egg-info') - c.run('python -m build --sdist') + shutil.rmtree('rattail_harvest.egg-info') + ctx.run('python setup.py sdist --formats=gztar') # upload to public PyPI - filename = f'rattail_harvest-{__version__}.tar.gz' - c.run(f'twine upload dist/{filename}') + filename = 'rattail-harvest-{}.tar.gz'.format(__version__) + ctx.run('twine upload dist/{}'.format(filename))