From 4181ede57c416dc6943778c18ad50d2ef4b02a9a Mon Sep 17 00:00:00 2001 From: Ryan Cheley Date: Wed, 5 Jan 2022 19:38:27 -0800 Subject: [PATCH 01/13] moved cli to src --- {toggl_to_sqlite => src/toggl_to_sqlite}/__init__.py | 0 {toggl_to_sqlite => src/toggl_to_sqlite}/cli.py | 0 {toggl_to_sqlite => src/toggl_to_sqlite}/utils.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {toggl_to_sqlite => src/toggl_to_sqlite}/__init__.py (100%) rename {toggl_to_sqlite => src/toggl_to_sqlite}/cli.py (100%) rename {toggl_to_sqlite => src/toggl_to_sqlite}/utils.py (100%) diff --git a/toggl_to_sqlite/__init__.py b/src/toggl_to_sqlite/__init__.py similarity index 100% rename from toggl_to_sqlite/__init__.py rename to src/toggl_to_sqlite/__init__.py diff --git a/toggl_to_sqlite/cli.py b/src/toggl_to_sqlite/cli.py similarity index 100% rename from toggl_to_sqlite/cli.py rename to src/toggl_to_sqlite/cli.py diff --git a/toggl_to_sqlite/utils.py b/src/toggl_to_sqlite/utils.py similarity index 100% rename from toggl_to_sqlite/utils.py rename to src/toggl_to_sqlite/utils.py From ca11826555a1af6e9ea7a1c78a55f16dd94a4c3f Mon Sep 17 00:00:00 2001 From: Ryan Cheley Date: Wed, 5 Jan 2022 19:54:10 -0800 Subject: [PATCH 02/13] updated to use cog --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 56cee74..9c03d6c 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,35 @@ To get your workspaces and projects: The default is to get all three of `time_entries`, `projects`, and `workspaces` +## toggl-to-sqlite --help + + +``` +Usage: toggl-to-sqlite [OPTIONS] COMMAND [ARGS]... + + Save Toggl data to a SQLite database + +Options: + --version Show the version and exit. + --help Show this message and exit. + +Commands: + auth Save authentication credentials to a JSON file + fetch Save Toggl data to a SQLite database + +``` + + ## Using with Datasette The SQLite database produced by this tool is designed to be browsed using [Datasette](https://datasette.readthedocs.io/). Use the [datasette-render-timestamps](https://github.com/simonw/datasette-render-timestamps) plugin to improve the display of the timestamp values. From aec1744e2716f6ffeef5481c7c06a432c080caf0 Mon Sep 17 00:00:00 2001 From: Ryan Cheley Date: Wed, 5 Jan 2022 19:55:46 -0800 Subject: [PATCH 03/13] Added infrastructure for easier contributing --- .coveragerc | 9 +++++++++ .isort.cfg | 2 ++ .pre-commit-config.yaml | 27 +++++++++++++++++++++++++++ pyproject.toml | 5 +++++ setup.cfg | 4 ++++ 5 files changed, 47 insertions(+) create mode 100644 .coveragerc create mode 100644 .isort.cfg create mode 100644 .pre-commit-config.yaml create mode 100644 pyproject.toml create mode 100644 setup.cfg diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..fa15d43 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,9 @@ +[run] +omit= + */venv/* + */htmlcov/* + +[report] +fail_under = 100 +show_missing = True +skip_covered = False diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..5d7bf33 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,2 @@ +[tool.isort] +profile = "black" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e32526b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +ci: + autoupdate_commit_msg: "chore: [pre-commit.ci] pre-commit autoupdate" + autoupdate_schedule: monthly + +repos: +- repo: https://github.com/psf/black + rev: 21.12b0 + hooks: + - id: black + language_version: python3.9 +- repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: + - id: flake8 +- repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + args: ["--profile", "black"] + name: isort (python) +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..612d5c2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[tool.black] +line-length = 130 +target-version = ['py39'] +include = '\.pyi?$' +exclude = '(venv)' diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..5c81bb6 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[flake8] +exclude = .git,*migrations*,*venv* +max-line-length = 130 +ignore = E203 From 73c54dadc29223a62d9a421daefa418bf713faa6 Mon Sep 17 00:00:00 2001 From: Ryan Cheley Date: Wed, 5 Jan 2022 19:56:56 -0800 Subject: [PATCH 04/13] Dropped stated support for Python 3.6 --- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3755c3a..5cc9b66 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.7, 3.8, 3.9, "3.10"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 76826b6..bc444a5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] + python-version: [3.7, 3.8, 3.9, "3.10"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} From bd43fa1ba84ad17b6436527503736e03e4dc6582 Mon Sep 17 00:00:00 2001 From: Ryan Cheley Date: Wed, 5 Jan 2022 19:57:13 -0800 Subject: [PATCH 05/13] Added more options for just --- justfile | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index 953d823..c7061f3 100644 --- a/justfile +++ b/justfile @@ -1,5 +1,31 @@ # run tests via pytest, creates coverage report, and then opens it up test: coverage run -m pytest - coverage html - open htmlcov/index.html \ No newline at end of file + coverage html --omit=src/toggl_to_sqlite/cli.py--omit=src/toggl_to_sqlite/cli.py + open htmlcov/index.html + +# runs the pre-commit check command +check: mypy + pre-commit run --all-files + +# opens the coverage index +coverage: + open htmlcov/index.html + +# prunes remote branches from github +prune: + git remote prune origin + +# removes all but main and dev local branch +gitclean: + git branch | grep -v "main" | grep -v "dev"| xargs git branch -D + + +# run mypy on the files +mypy: + mypy src/toggl_to_sqlite/*.py --no-strict-optional + + +# generates the README.md file --help section +cog: + cog -r README.md From b691a40a680c34fe85bd12a9c302f94a87116747 Mon Sep 17 00:00:00 2001 From: Ryan Cheley Date: Wed, 5 Jan 2022 19:58:13 -0800 Subject: [PATCH 06/13] Formatting Changes --- src/toggl_to_sqlite/cli.py | 8 ++++---- src/toggl_to_sqlite/utils.py | 25 ++++++++----------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/toggl_to_sqlite/cli.py b/src/toggl_to_sqlite/cli.py index d3ea666..1815786 100644 --- a/src/toggl_to_sqlite/cli.py +++ b/src/toggl_to_sqlite/cli.py @@ -1,6 +1,8 @@ -import click import json + +import click import sqlite_utils + from . import utils @@ -68,9 +70,7 @@ def fetch(db_path, auth, days, since, type): days = days since = since if "time_entries" in type: - time_entries = utils.get_time_entries( - api_token=auth["api_token"], days=days, since=since - ) + time_entries = utils.get_time_entries(api_token=auth["api_token"], days=days, since=since) utils.save_items(time_entries, "time_entries", db) if "workspaces" in type: workspaces = utils.get_workspaces(api_token=auth["api_token"]) diff --git a/src/toggl_to_sqlite/utils.py b/src/toggl_to_sqlite/utils.py index ace90c8..8a4786b 100644 --- a/src/toggl_to_sqlite/utils.py +++ b/src/toggl_to_sqlite/utils.py @@ -1,20 +1,17 @@ -import requests -import json import datetime +import json import math +import requests + def get_start_datetime(api_token, since: datetime = None): - toggl = requests.get( - "https://api.track.toggl.com/api/v8/me", auth=(api_token, "api_token") - ) + toggl = requests.get("https://api.track.toggl.com/api/v8/me", auth=(api_token, "api_token")) if toggl.status_code == 200: data = json.loads(toggl.text) if not since: start_time = data["data"]["workspaces"][0]["at"] - start_time = datetime.datetime.strptime( - start_time, "%Y-%m-%dT%H:%M:%S+00:00" - ) + start_time = datetime.datetime.strptime(start_time, "%Y-%m-%dT%H:%M:%S+00:00") else: start_time = since return start_time.date() @@ -24,9 +21,7 @@ def get_start_datetime(api_token, since: datetime = None): def get_workspaces(api_token): workspaces = [] - response = requests.get( - "https://api.track.toggl.com/api/v8/workspaces", auth=(api_token, "api_token") - ) + response = requests.get("https://api.track.toggl.com/api/v8/workspaces", auth=(api_token, "api_token")) if response.status_code == 200: workspaces.append(json.loads(response.text)) for workspace in workspaces[0]: @@ -60,12 +55,8 @@ def get_time_entries(api_token, days, since: datetime = None): if days > 0: cycles = math.ceil((today - start_date).days / days) for cycle in range(cycles): - _start_date = (start_date + datetime.timedelta(days=days) * cycle).strftime( - "%Y-%m-%dT00:00:00-00:00" - ) - _end_date = ( - start_date + datetime.timedelta(days=days) * (cycle + 1) - ).strftime("%Y-%m-%dT00:00:00-00:00") + _start_date = (start_date + datetime.timedelta(days=days) * cycle).strftime("%Y-%m-%dT00:00:00-00:00") + _end_date = (start_date + datetime.timedelta(days=days) * (cycle + 1)).strftime("%Y-%m-%dT00:00:00-00:00") params = ( ("start_date", _start_date), ("end_date", _end_date), From da561302fb00db8445f51fdf6ef74c3cf10b4edb Mon Sep 17 00:00:00 2001 From: Ryan Cheley Date: Wed, 5 Jan 2022 19:58:39 -0800 Subject: [PATCH 07/13] Infrastructure updates --- setup.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index ac99825..73cdcb2 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,8 @@ -from setuptools import setup import os -VERSION = "0.5" +from setuptools import setup + +VERSION = "0.6.0" def get_long_description(): @@ -18,20 +19,33 @@ def get_long_description(): long_description=get_long_description(), long_description_content_type="text/markdown", author="Ryan Cheley", + project_urls={ + "Issues": "https://github.com/ryancheley/toggl-to-sqlite/issues", + "CI": "https://github.com/ryancheley/toggl-to-sqlite/actions", + "Changelog": "https://github.com/ryancheley/toggl-to-sqlite/releases", + "Documentation": "https://github.com/ryancheley/toggl-to-sqlite/blob/main/README.md", + }, url="https://github.com/ryancheley/toggle-to-sqlite", license="Apache License, Version 2.0", version=VERSION, packages=["toggl_to_sqlite"], + package_dir={"": "src"}, entry_points=""" [console_scripts] toggl-to-sqlite=toggl_to_sqlite.cli:cli """, - install_requires=["sqlite-utils>=2.4.4", "click", "requests", "requests_mock"], - extras_require={ - "test": [ - "pytest", - "black==21.5b2", - ] - }, + install_requires=["sqlite-utils>=2.4.4", "click", "requests", "requests_mock", "toml"], + extras_require={"test": ["pytest", "black", "isort", "coverage", "mypy", "cogapp"]}, tests_require=["toggl-to-sqlite[test]"], + python_requires=">=3.6", + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Utilities", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + ], + ) From c378632269b9ad1b6180df05aa128741788076de Mon Sep 17 00:00:00 2001 From: Ryan Cheley Date: Wed, 5 Jan 2022 19:59:01 -0800 Subject: [PATCH 08/13] updates to exclude more files --- .gitignore | 143 +++++------------------------------------------------ 1 file changed, 11 insertions(+), 132 deletions(-) diff --git a/.gitignore b/.gitignore index 3cd7662..99b81f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,139 +1,18 @@ -# Byte-compiled / optimized / DLL files +.venv __pycache__/ *.py[cod] *$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -.vscode +venv +.eggs +.pytest_cache +*.egg-info .DS_Store +.coverage auth.json -.coveragerc +dist *.db -tests/test_hidden.py +.mutmut-cache -# Jupyter Notebooks -*.ipynb +# ignore the output files generated +output.html +output.txt From 6be5c6f1394d84a48d4c8d1103581174a44a4e05 Mon Sep 17 00:00:00 2001 From: Ryan Cheley Date: Wed, 5 Jan 2022 19:59:11 -0800 Subject: [PATCH 09/13] formatting changes --- tests/test_toggl_to_sqlite.py | 41 +++++++++-------------------------- tests/toggl.json | 2 +- 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/tests/test_toggl_to_sqlite.py b/tests/test_toggl_to_sqlite.py index fce74ea..ba71684 100644 --- a/tests/test_toggl_to_sqlite.py +++ b/tests/test_toggl_to_sqlite.py @@ -1,13 +1,12 @@ -from toggl_to_sqlite import utils -import pytest +import datetime import json -import sqlite_utils -from sqlite_utils.db import ForeignKey import pathlib -import requests -import datetime -import random + +import pytest import requests_mock +import sqlite_utils + +from toggl_to_sqlite import utils def load(): @@ -85,24 +84,8 @@ def mock_get_workspaces(api_token): def test_get_time_entries_bad_api(): api_token = "api_token" actual = utils.get_time_entries(api_token=api_token, days=25) - expepected = [] - assert actual == expepected - - -def test_get_time_entries_good_api(monkeypatch): - expected_time_entires = [ - { - "data": { - "id": 436694100, - "pid": 123, - "wid": 777, - "start": "2013-03-05T07:58:58.000Z", - "duration": 1200, - "description": "Meeting with possible clients", - "tags": ["billed"], - } - } - ] + expected = [] + assert actual == expected def test_get_get_workspaces(): @@ -180,9 +163,7 @@ def test_get_start_datetime_with_good_api_token_and_since(): "workspaces": [{"at": "2021-01-01T15:35:47+00:00"}], } } - rm.get( - "https://api.track.toggl.com/api/v8/me", status_code=200, json=return_value - ) + rm.get("https://api.track.toggl.com/api/v8/me", status_code=200, json=return_value) response = utils.get_start_datetime("api_token", expected_start_time) assert response == expected_start_time.date() @@ -204,9 +185,7 @@ def test_get_start_datetime_with_good_api_token_and_blank_since(): "workspaces": [{"at": "2021-01-01T15:35:47+00:00"}], } } - rm.get( - "https://api.track.toggl.com/api/v8/me", status_code=200, json=return_value - ) + rm.get("https://api.track.toggl.com/api/v8/me", status_code=200, json=return_value) response = utils.get_start_datetime("api_token") print(response) diff --git a/tests/toggl.json b/tests/toggl.json index f08b0d6..c89b6f3 100644 --- a/tests/toggl.json +++ b/tests/toggl.json @@ -23,4 +23,4 @@ "at":"2013-03-12T14:32:43+00:00" } ] -] \ No newline at end of file +] From 2ed5275155aaab0e0f8cb68bb8b29c20f29f800c Mon Sep 17 00:00:00 2001 From: Ryan Cheley Date: Mon, 10 Jan 2022 18:24:04 -0800 Subject: [PATCH 10/13] chore: test refactoring --- tests/test_classes.py | 48 +++++++++ tests/test_toggl_to_sqlite.py | 193 ---------------------------------- tests/test_utils.py | 151 ++++++++++++++++++++++++++ 3 files changed, 199 insertions(+), 193 deletions(-) create mode 100644 tests/test_classes.py delete mode 100644 tests/test_toggl_to_sqlite.py create mode 100644 tests/test_utils.py diff --git a/tests/test_classes.py b/tests/test_classes.py new file mode 100644 index 0000000..716f901 --- /dev/null +++ b/tests/test_classes.py @@ -0,0 +1,48 @@ +class MockResponseGetStartDateTime: + def __init__(self, status_code) -> None: + self.status_code = status_code + @staticmethod + def json(): + return { + "data": { + "workspaces": [{ + "at": "2019-12-04T05:14:38+00:00" + }] + } + } + + +class MockResponseWorkspaces: + def __init__(self, status_code) -> None: + self.status_code = status_code + @staticmethod + def json(): + return [ + { + 'id': 1, + 'name': "TEST workspace", + 'profile': 0, + 'premium': False, + 'admin': True, + 'default_hourly_rate': 0, + 'default_currency': 'USD', + 'only_admins_may_create_projects': False, + 'only_admins_see_billable_rates': False, + 'only_admins_see_team_dashboard': False, + 'projects_billable_by_default': True, + 'rounding': 1, + 'rounding_minutes': 0, + 'api_token': 'fake_api', + 'at': '2016-12-15T06:53:39+00:00', + 'ical_enabled': True + } + ] + + +class MockTimeEntryResponse: + def __init__(self, status_code) -> None: + self.status_code = status_code + + @staticmethod + def json(): + return [{'id': 2306806385, 'guid': 'e13d9d8b6aee8c8363c80608a93d9bfd', 'wid': 3829545, 'pid': 157777117, 'billable': False, 'start': '2022-01-03T16:00:00+00:00', 'stop': '2022-01-04T00:00:00+00:00', 'duration': 28800, 'description': 'PTO', 'duronly': False, 'at': '2021-12-28T22:58:14+00:00', 'uid': 2611584}, {'id': 2311598789, 'guid': '58ea59f21eb26166195bd887dc2f2270', 'wid': 3829545, 'pid': 155736600, 'billable': False, 'start': '2022-01-04T15:57:15+00:00', 'stop': '2022-01-04T16:01:07+00:00', 'duration': 232, 'description': 'Email', 'duronly': False, 'at': '2022-01-04T16:01:07+00:00', 'uid': 2611584}, {'id': 2311606757, 'guid': 'd50f74fb306ba0d9a79b68975e1fe5d0', 'wid': 3829545, 'pid': 155736600, 'billable': False, 'start': '2022-01-04T16:01:07+00:00', 'stop': '2022-01-04T16:28:16+00:00', 'duration': 1629, 'description': 'Computer Restart', 'duronly': False, 'at': '2022-01-04T16:28:16+00:00', 'uid': 2611584}] \ No newline at end of file diff --git a/tests/test_toggl_to_sqlite.py b/tests/test_toggl_to_sqlite.py deleted file mode 100644 index ba71684..0000000 --- a/tests/test_toggl_to_sqlite.py +++ /dev/null @@ -1,193 +0,0 @@ -import datetime -import json -import pathlib - -import pytest -import requests_mock -import sqlite_utils - -from toggl_to_sqlite import utils - - -def load(): - json_path = pathlib.Path(__file__).parent / "toggl.json" - return json.load(open(json_path, "r")) - - -@pytest.fixture(scope="session") -def converted(): - db = sqlite_utils.Database(":memory:") - utils.save_items(load(), "time_entries", db) - return db - - -def test_tables(converted): - assert {"time_entries"} == set(converted.table_names()) - - -def test_item(converted): - item = list(converted["time_entries"].rows)[0] - print(item) - assert { - "id": 436691234, - "wid": 777, - "pid": 123, - "billable": 1, - "start": "2013-03-11T11:36:00+00:00", - "stop": "2013-03-11T15:36:00+00:00", - "duration": 14400, - "description": "Meeting with the client", - "tags": '["tag1, tag2"]', - "at": "2013-03-11T15:36:58+00:00", - } == item - - -def test_get_start_datetime_bad_api_token(): - api_token = "api_token" - start_date = utils.get_start_datetime(api_token=api_token) - assert start_date == datetime.date.today() - - -def test_get_workspaces_bad_api_token(): - api_token = "api_token" - workspaces = utils.get_workspaces(api_token=api_token) - assert workspaces == [] - - -def test_get_projects_bad_api(): - api_token = "api_token" - actual = utils.get_projects(api_token=api_token) - expected = [] - assert actual == expected - - -def test_get_projects_good_api(monkeypatch): - execpted_projects = [{"id": 1}] - - def mock_get_workspaces(api_token): - return [[{"id": 1}]] - - monkeypatch.setattr(utils, "get_workspaces", mock_get_workspaces) - - with requests_mock.Mocker() as rm: - return_value = {"id": 1} - rm.get( - "https://api.track.toggl.com/api/v8/workspaces/1/projects", - status_code=200, - json=return_value, - ) - response = utils.get_projects("api_token") - - assert execpted_projects == response - - -def test_get_time_entries_bad_api(): - api_token = "api_token" - actual = utils.get_time_entries(api_token=api_token, days=25) - expected = [] - assert actual == expected - - -def test_get_get_workspaces(): - expected_workspaces = [ - { - "data": [ - { - "id": 3134975, - "name": "John's personal ws", - "default_hourly_rate": 50, - "default_currency": "USD", - "rounding": 1, - "rounding_minutes": 15, - "at": "2013-08-28T16:22:21+00:00", - "logo_url": "my_logo.png", - }, - { - "id": 777, - "name": "My Company Inc", - "default_hourly_rate": 40, - "default_currency": "EUR", - "rounding": 1, - "rounding_minutes": 15, - "at": "2013-08-28T16:22:21+00:00", - }, - ] - } - ] - with requests_mock.Mocker() as rm: - return_value = { - "data": [ - { - "id": 3134975, - "name": "John's personal ws", - "default_hourly_rate": 50, - "default_currency": "USD", - "rounding": 1, - "rounding_minutes": 15, - "at": "2013-08-28T16:22:21+00:00", - "logo_url": "my_logo.png", - }, - { - "id": 777, - "name": "My Company Inc", - "default_hourly_rate": 40, - "default_currency": "EUR", - "rounding": 1, - "rounding_minutes": 15, - "at": "2013-08-28T16:22:21+00:00", - }, - ] - } - rm.get( - "https://api.track.toggl.com/api/v8/workspaces", - status_code=200, - json=return_value, - ) - response = utils.get_workspaces("api_token") - assert response == expected_workspaces - - -def test_get_start_datetime_with_good_api_token_and_since(): - expected_start_time = datetime.datetime(2021, 4, 1, 0, 0) - with requests_mock.Mocker() as rm: - return_value = { - "data": { - "description": "New time entry", - "start": "2013-02-12T15:35:47+02:00", - "wid": 31366, - "pid": 9012, - "duration": 1200, - "stop": "2013-02-12T15:35:57+02:00", - "tags": ["billed"], - "id": 4269795, - "workspaces": [{"at": "2021-01-01T15:35:47+00:00"}], - } - } - rm.get("https://api.track.toggl.com/api/v8/me", status_code=200, json=return_value) - - response = utils.get_start_datetime("api_token", expected_start_time) - assert response == expected_start_time.date() - - -def test_get_start_datetime_with_good_api_token_and_blank_since(): - expected_start_time = datetime.date(2021, 1, 1) - with requests_mock.Mocker() as rm: - return_value = { - "data": { - "description": "New time entry", - "start": "2013-02-12T15:35:47+02:00", - "wid": 31366, - "pid": 9012, - "duration": 1200, - "stop": "2013-02-12T15:35:57+02:00", - "tags": ["billed"], - "id": 4269795, - "workspaces": [{"at": "2021-01-01T15:35:47+00:00"}], - } - } - rm.get("https://api.track.toggl.com/api/v8/me", status_code=200, json=return_value) - - response = utils.get_start_datetime("api_token") - print(response) - - assert response == expected_start_time diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..4666381 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,151 @@ +import json +import pathlib + +import datetime +from math import exp +import pytest +import requests +import sqlite_utils + +from test_classes import ( + MockResponseGetStartDateTime, + MockResponseWorkspaces, + MockTimeEntryResponse +) + +from toggl_to_sqlite.utils import ( + get_start_datetime, + get_workspaces, + get_projects, + get_time_entries, + save_items, +) + + +def load(): + json_path = pathlib.Path(__file__).parent / "toggl.json" + return json.load(open(json_path, "r")) + + +@pytest.fixture(scope="session") +def converted(): + db = sqlite_utils.Database(":memory:") + save_items(load(), "time_entries", db) + return db + +def test_tables(converted): + assert {"time_entries"} == set(converted.table_names()) + + +def test_item(converted): + item = list(converted["time_entries"].rows)[0] + print(item) + assert { + "id": 436691234, + "wid": 777, + "pid": 123, + "billable": 1, + "start": "2013-03-11T11:36:00+00:00", + "stop": "2013-03-11T15:36:00+00:00", + "duration": 14400, + "description": "Meeting with the client", + "tags": '["tag1, tag2"]', + "at": "2013-03-11T15:36:58+00:00", + } == item + + +def test_get_time_entries(monkeypatch): + api_token = 'fake_api' + days = 10 + + def mock_get(*args, **kwargs): + return MockTimeEntryResponse(200) + + def mock_get_start_datetime(api_token=api_token, days=days): + return datetime.date(2022, 1, 1) + + monkeypatch.setattr(requests, "get", mock_get) + monkeypatch.setattr("toggl_to_sqlite.utils.get_start_datetime", mock_get_start_datetime) + actual = get_time_entries(api_token=api_token, days=days) + expected = [[{'id': 2306806385, 'guid': 'e13d9d8b6aee8c8363c80608a93d9bfd', 'wid': 3829545, 'pid': 157777117, 'billable': False, 'start': '2022-01-03T16:00:00+00:00', 'stop': '2022-01-04T00:00:00+00:00', 'duration': 28800, 'description': 'PTO', 'duronly': False, 'at': '2021-12-28T22:58:14+00:00', 'uid': 2611584}, {'id': 2311598789, 'guid': '58ea59f21eb26166195bd887dc2f2270', 'wid': 3829545, 'pid': 155736600, 'billable': False, 'start': '2022-01-04T15:57:15+00:00', 'stop': '2022-01-04T16:01:07+00:00', 'duration': 232, 'description': 'Email', 'duronly': False, 'at': '2022-01-04T16:01:07+00:00', 'uid': 2611584}, {'id': 2311606757, 'guid': 'd50f74fb306ba0d9a79b68975e1fe5d0', 'wid': 3829545, 'pid': 155736600, 'billable': False, 'start': '2022-01-04T16:01:07+00:00', 'stop': '2022-01-04T16:28:16+00:00', 'duration': 1629, 'description': 'Computer Restart', 'duronly': False, 'at': '2022-01-04T16:28:16+00:00', 'uid': 2611584}]] + assert actual == expected + + + +def test_get_projects(monkeypatch): + api_token = 'fake_api' + + def mock_get(*args, **kwargs): + return MockResponseWorkspaces(200) + + + def mock_get_workspaces(api_token=api_token): + return [[{'id': 1806100}]] + + monkeypatch.setattr(requests, "get", mock_get) + monkeypatch.setattr("toggl_to_sqlite.utils.get_workspaces", mock_get_workspaces) + expected = [[{'id': 1, 'name': 'TEST workspace', 'profile': 0, 'premium': False, 'admin': True, 'default_hourly_rate': 0, 'default_currency': 'USD', 'only_admins_may_create_projects': False, 'only_admins_see_billable_rates': False, 'only_admins_see_team_dashboard': False, 'projects_billable_by_default': True, 'rounding': 1, 'rounding_minutes': 0, 'api_token': 'fake_api', 'at': '2016-12-15T06:53:39+00:00', 'ical_enabled': True}]] + actual = get_projects(api_token=api_token) + assert actual == expected + + +def test_get_workspaces(monkeypatch): + + def mock_get(*args, **kwargs): + return MockResponseWorkspaces(200) + + api_token = 'fake_api' + monkeypatch.setattr(requests, "get", mock_get) + actual = get_workspaces(api_token=api_token) + assert "api_token" not in actual[0][0].keys() + + + +def test_get_start_datetime_no_since_passed_with_response(monkeypatch): + + def mock_get(*args, **kwargs): + return MockResponseGetStartDateTime(200) + + api_token = 'fake_api' + monkeypatch.setattr(requests, "get", mock_get) + expected = datetime.date(2019, 12, 4) + actual = get_start_datetime(api_token=api_token) + assert actual == expected + + +def test_get_start_datetime_no_since_passed_with_no_response(monkeypatch): + + def mock_get(*args, **kwargs): + return MockResponseGetStartDateTime(404) + + api_token = 'fake_api' + monkeypatch.setattr(requests, "get", mock_get) + expected = datetime.date.today() + actual = get_start_datetime(api_token=api_token) + assert actual == expected + + +def test_get_start_datetime_since_passed_with_response(monkeypatch): + + def mock_get(*args, **kwargs): + return MockResponseGetStartDateTime(200) + + api_token = 'fake_api' + since = datetime.datetime(2020, 3, 13) + monkeypatch.setattr(requests, "get", mock_get) + expected = datetime.date(2020, 3, 13) + actual = get_start_datetime(api_token=api_token, since=since) + assert actual == expected + + +def test_get_start_datetime_since_passed_with_no_response(monkeypatch): + + def mock_get(*args, **kwargs): + return MockResponseGetStartDateTime(404) + + api_token = 'fake_api' + since = datetime.datetime.now() + monkeypatch.setattr(requests, "get", mock_get) + expected = datetime.date.today() + actual = get_start_datetime(api_token=api_token, since=since) + assert actual == expected From 4a0395f6a1b006470124a848b163c4acd21444b7 Mon Sep 17 00:00:00 2001 From: Ryan Cheley Date: Mon, 10 Jan 2022 18:27:05 -0800 Subject: [PATCH 11/13] chore: linting and formatting --- README.md | 7 +-- contributing.md | 2 +- setup.cfg | 2 +- setup.py | 1 - src/toggl_to_sqlite/utils.py | 14 ++--- tests/test_classes.py | 89 +++++++++++++++++++-------- tests/test_utils.py | 115 ++++++++++++++++++++++++++--------- 7 files changed, 160 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 9c03d6c..01e1be9 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ You will need to first obtain a valid API token for your toggl account. You can https://track.toggl.com/profile - Once you have your API Token enter it at the command line. - + Once you have your API Token enter it at the command line. + Authentication tokens written to auth.json Now you can fetch all of your items from toggl like this: @@ -33,7 +33,7 @@ Now you can fetch all of your items from toggl like this: $ toggl-to-sqlite fetch -s 2021-03-13 -You can choose to get only `time_entries`, `projects`, or `workspaces` by speciying a type in the argument like this. +You can choose to get only `time_entries`, `projects`, or `workspaces` by speciying a type in the argument like this. To get ONLY your workspaces: @@ -77,4 +77,3 @@ Commands: ## Using with Datasette The SQLite database produced by this tool is designed to be browsed using [Datasette](https://datasette.readthedocs.io/). Use the [datasette-render-timestamps](https://github.com/simonw/datasette-render-timestamps) plugin to improve the display of the timestamp values. - diff --git a/contributing.md b/contributing.md index 1181595..fc21477 100644 --- a/contributing.md +++ b/contributing.md @@ -18,4 +18,4 @@ To run the tests: ## Code style -This library uses [Black](https://github.com/psf/black) for code formatting. The correct version of Black will be installed by `pip install -e '.[test]'` - you can run `black .` in the root directory to apply those formatting rules. \ No newline at end of file +This library uses [Black](https://github.com/psf/black) for code formatting. The correct version of Black will be installed by `pip install -e '.[test]'` - you can run `black .` in the root directory to apply those formatting rules. diff --git a/setup.cfg b/setup.cfg index 5c81bb6..dff6111 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ [flake8] exclude = .git,*migrations*,*venv* max-line-length = 130 -ignore = E203 +ignore = E203,W503 diff --git a/setup.py b/setup.py index 73cdcb2..baa7f24 100644 --- a/setup.py +++ b/setup.py @@ -47,5 +47,4 @@ def get_long_description(): "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", ], - ) diff --git a/src/toggl_to_sqlite/utils.py b/src/toggl_to_sqlite/utils.py index 8a4786b..5e9a99c 100644 --- a/src/toggl_to_sqlite/utils.py +++ b/src/toggl_to_sqlite/utils.py @@ -1,5 +1,4 @@ import datetime -import json import math import requests @@ -8,7 +7,7 @@ def get_start_datetime(api_token, since: datetime = None): toggl = requests.get("https://api.track.toggl.com/api/v8/me", auth=(api_token, "api_token")) if toggl.status_code == 200: - data = json.loads(toggl.text) + data = toggl.json() if not since: start_time = data["data"]["workspaces"][0]["at"] start_time = datetime.datetime.strptime(start_time, "%Y-%m-%dT%H:%M:%S+00:00") @@ -23,12 +22,9 @@ def get_workspaces(api_token): workspaces = [] response = requests.get("https://api.track.toggl.com/api/v8/workspaces", auth=(api_token, "api_token")) if response.status_code == 200: - workspaces.append(json.loads(response.text)) + workspaces.append(response.json()) for workspace in workspaces[0]: - try: - workspace.pop("api_token", None) - except AttributeError: - pass + workspace.pop("api_token", None) return workspaces @@ -42,7 +38,7 @@ def get_projects(api_token): params={"active": "both"}, auth=(api_token, "api_token"), ) - project = json.loads(response.text) + project = response.json() if project: projects.append(project) return projects @@ -66,7 +62,7 @@ def get_time_entries(api_token, days, since: datetime = None): params=params, auth=(api_token, "api_token"), ) - data.append(json.loads(response.text)) + data.append(response.json()) return data diff --git a/tests/test_classes.py b/tests/test_classes.py index 716f901..d3db83e 100644 --- a/tests/test_classes.py +++ b/tests/test_classes.py @@ -1,40 +1,36 @@ class MockResponseGetStartDateTime: def __init__(self, status_code) -> None: self.status_code = status_code + @staticmethod def json(): - return { - "data": { - "workspaces": [{ - "at": "2019-12-04T05:14:38+00:00" - }] - } - } + return {"data": {"workspaces": [{"at": "2019-12-04T05:14:38+00:00"}]}} class MockResponseWorkspaces: def __init__(self, status_code) -> None: self.status_code = status_code + @staticmethod def json(): return [ { - 'id': 1, - 'name': "TEST workspace", - 'profile': 0, - 'premium': False, - 'admin': True, - 'default_hourly_rate': 0, - 'default_currency': 'USD', - 'only_admins_may_create_projects': False, - 'only_admins_see_billable_rates': False, - 'only_admins_see_team_dashboard': False, - 'projects_billable_by_default': True, - 'rounding': 1, - 'rounding_minutes': 0, - 'api_token': 'fake_api', - 'at': '2016-12-15T06:53:39+00:00', - 'ical_enabled': True + "id": 1, + "name": "TEST workspace", + "profile": 0, + "premium": False, + "admin": True, + "default_hourly_rate": 0, + "default_currency": "USD", + "only_admins_may_create_projects": False, + "only_admins_see_billable_rates": False, + "only_admins_see_team_dashboard": False, + "projects_billable_by_default": True, + "rounding": 1, + "rounding_minutes": 0, + "api_token": "fake_api", + "at": "2016-12-15T06:53:39+00:00", + "ical_enabled": True, } ] @@ -42,7 +38,50 @@ def json(): class MockTimeEntryResponse: def __init__(self, status_code) -> None: self.status_code = status_code - + @staticmethod def json(): - return [{'id': 2306806385, 'guid': 'e13d9d8b6aee8c8363c80608a93d9bfd', 'wid': 3829545, 'pid': 157777117, 'billable': False, 'start': '2022-01-03T16:00:00+00:00', 'stop': '2022-01-04T00:00:00+00:00', 'duration': 28800, 'description': 'PTO', 'duronly': False, 'at': '2021-12-28T22:58:14+00:00', 'uid': 2611584}, {'id': 2311598789, 'guid': '58ea59f21eb26166195bd887dc2f2270', 'wid': 3829545, 'pid': 155736600, 'billable': False, 'start': '2022-01-04T15:57:15+00:00', 'stop': '2022-01-04T16:01:07+00:00', 'duration': 232, 'description': 'Email', 'duronly': False, 'at': '2022-01-04T16:01:07+00:00', 'uid': 2611584}, {'id': 2311606757, 'guid': 'd50f74fb306ba0d9a79b68975e1fe5d0', 'wid': 3829545, 'pid': 155736600, 'billable': False, 'start': '2022-01-04T16:01:07+00:00', 'stop': '2022-01-04T16:28:16+00:00', 'duration': 1629, 'description': 'Computer Restart', 'duronly': False, 'at': '2022-01-04T16:28:16+00:00', 'uid': 2611584}] \ No newline at end of file + return [ + { + "id": 2306806385, + "guid": "e13d9d8b6aee8c8363c80608a93d9bfd", + "wid": 3829545, + "pid": 157777117, + "billable": False, + "start": "2022-01-03T16:00:00+00:00", + "stop": "2022-01-04T00:00:00+00:00", + "duration": 28800, + "description": "PTO", + "duronly": False, + "at": "2021-12-28T22:58:14+00:00", + "uid": 2611584, + }, + { + "id": 2311598789, + "guid": "58ea59f21eb26166195bd887dc2f2270", + "wid": 3829545, + "pid": 155736600, + "billable": False, + "start": "2022-01-04T15:57:15+00:00", + "stop": "2022-01-04T16:01:07+00:00", + "duration": 232, + "description": "Email", + "duronly": False, + "at": "2022-01-04T16:01:07+00:00", + "uid": 2611584, + }, + { + "id": 2311606757, + "guid": "d50f74fb306ba0d9a79b68975e1fe5d0", + "wid": 3829545, + "pid": 155736600, + "billable": False, + "start": "2022-01-04T16:01:07+00:00", + "stop": "2022-01-04T16:28:16+00:00", + "duration": 1629, + "description": "Computer Restart", + "duronly": False, + "at": "2022-01-04T16:28:16+00:00", + "uid": 2611584, + }, + ] diff --git a/tests/test_utils.py b/tests/test_utils.py index 4666381..ff70260 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,23 +1,21 @@ +import datetime import json import pathlib -import datetime -from math import exp import pytest import requests import sqlite_utils - from test_classes import ( MockResponseGetStartDateTime, MockResponseWorkspaces, - MockTimeEntryResponse + MockTimeEntryResponse, ) from toggl_to_sqlite.utils import ( - get_start_datetime, - get_workspaces, get_projects, + get_start_datetime, get_time_entries, + get_workspaces, save_items, ) @@ -33,6 +31,7 @@ def converted(): save_items(load(), "time_entries", db) return db + def test_tables(converted): assert {"time_entries"} == set(converted.table_names()) @@ -55,7 +54,7 @@ def test_item(converted): def test_get_time_entries(monkeypatch): - api_token = 'fake_api' + api_token = "fake_api" days = 10 def mock_get(*args, **kwargs): @@ -67,85 +66,143 @@ def mock_get_start_datetime(api_token=api_token, days=days): monkeypatch.setattr(requests, "get", mock_get) monkeypatch.setattr("toggl_to_sqlite.utils.get_start_datetime", mock_get_start_datetime) actual = get_time_entries(api_token=api_token, days=days) - expected = [[{'id': 2306806385, 'guid': 'e13d9d8b6aee8c8363c80608a93d9bfd', 'wid': 3829545, 'pid': 157777117, 'billable': False, 'start': '2022-01-03T16:00:00+00:00', 'stop': '2022-01-04T00:00:00+00:00', 'duration': 28800, 'description': 'PTO', 'duronly': False, 'at': '2021-12-28T22:58:14+00:00', 'uid': 2611584}, {'id': 2311598789, 'guid': '58ea59f21eb26166195bd887dc2f2270', 'wid': 3829545, 'pid': 155736600, 'billable': False, 'start': '2022-01-04T15:57:15+00:00', 'stop': '2022-01-04T16:01:07+00:00', 'duration': 232, 'description': 'Email', 'duronly': False, 'at': '2022-01-04T16:01:07+00:00', 'uid': 2611584}, {'id': 2311606757, 'guid': 'd50f74fb306ba0d9a79b68975e1fe5d0', 'wid': 3829545, 'pid': 155736600, 'billable': False, 'start': '2022-01-04T16:01:07+00:00', 'stop': '2022-01-04T16:28:16+00:00', 'duration': 1629, 'description': 'Computer Restart', 'duronly': False, 'at': '2022-01-04T16:28:16+00:00', 'uid': 2611584}]] + expected = [ + [ + { + "id": 2306806385, + "guid": "e13d9d8b6aee8c8363c80608a93d9bfd", + "wid": 3829545, + "pid": 157777117, + "billable": False, + "start": "2022-01-03T16:00:00+00:00", + "stop": "2022-01-04T00:00:00+00:00", + "duration": 28800, + "description": "PTO", + "duronly": False, + "at": "2021-12-28T22:58:14+00:00", + "uid": 2611584, + }, + { + "id": 2311598789, + "guid": "58ea59f21eb26166195bd887dc2f2270", + "wid": 3829545, + "pid": 155736600, + "billable": False, + "start": "2022-01-04T15:57:15+00:00", + "stop": "2022-01-04T16:01:07+00:00", + "duration": 232, + "description": "Email", + "duronly": False, + "at": "2022-01-04T16:01:07+00:00", + "uid": 2611584, + }, + { + "id": 2311606757, + "guid": "d50f74fb306ba0d9a79b68975e1fe5d0", + "wid": 3829545, + "pid": 155736600, + "billable": False, + "start": "2022-01-04T16:01:07+00:00", + "stop": "2022-01-04T16:28:16+00:00", + "duration": 1629, + "description": "Computer Restart", + "duronly": False, + "at": "2022-01-04T16:28:16+00:00", + "uid": 2611584, + }, + ] + ] assert actual == expected - def test_get_projects(monkeypatch): - api_token = 'fake_api' + api_token = "fake_api" def mock_get(*args, **kwargs): return MockResponseWorkspaces(200) - def mock_get_workspaces(api_token=api_token): - return [[{'id': 1806100}]] + return [[{"id": 1806100}]] monkeypatch.setattr(requests, "get", mock_get) monkeypatch.setattr("toggl_to_sqlite.utils.get_workspaces", mock_get_workspaces) - expected = [[{'id': 1, 'name': 'TEST workspace', 'profile': 0, 'premium': False, 'admin': True, 'default_hourly_rate': 0, 'default_currency': 'USD', 'only_admins_may_create_projects': False, 'only_admins_see_billable_rates': False, 'only_admins_see_team_dashboard': False, 'projects_billable_by_default': True, 'rounding': 1, 'rounding_minutes': 0, 'api_token': 'fake_api', 'at': '2016-12-15T06:53:39+00:00', 'ical_enabled': True}]] + expected = [ + [ + { + "id": 1, + "name": "TEST workspace", + "profile": 0, + "premium": False, + "admin": True, + "default_hourly_rate": 0, + "default_currency": "USD", + "only_admins_may_create_projects": False, + "only_admins_see_billable_rates": False, + "only_admins_see_team_dashboard": False, + "projects_billable_by_default": True, + "rounding": 1, + "rounding_minutes": 0, + "api_token": "fake_api", + "at": "2016-12-15T06:53:39+00:00", + "ical_enabled": True, + } + ] + ] actual = get_projects(api_token=api_token) assert actual == expected def test_get_workspaces(monkeypatch): - def mock_get(*args, **kwargs): return MockResponseWorkspaces(200) - api_token = 'fake_api' + api_token = "fake_api" monkeypatch.setattr(requests, "get", mock_get) actual = get_workspaces(api_token=api_token) assert "api_token" not in actual[0][0].keys() - - -def test_get_start_datetime_no_since_passed_with_response(monkeypatch): +def test_get_start_datetime_no_since_passed_with_response(monkeypatch): def mock_get(*args, **kwargs): return MockResponseGetStartDateTime(200) - api_token = 'fake_api' + api_token = "fake_api" monkeypatch.setattr(requests, "get", mock_get) expected = datetime.date(2019, 12, 4) actual = get_start_datetime(api_token=api_token) - assert actual == expected + assert actual == expected def test_get_start_datetime_no_since_passed_with_no_response(monkeypatch): - def mock_get(*args, **kwargs): return MockResponseGetStartDateTime(404) - api_token = 'fake_api' + api_token = "fake_api" monkeypatch.setattr(requests, "get", mock_get) expected = datetime.date.today() actual = get_start_datetime(api_token=api_token) - assert actual == expected + assert actual == expected def test_get_start_datetime_since_passed_with_response(monkeypatch): - def mock_get(*args, **kwargs): return MockResponseGetStartDateTime(200) - api_token = 'fake_api' + api_token = "fake_api" since = datetime.datetime(2020, 3, 13) monkeypatch.setattr(requests, "get", mock_get) expected = datetime.date(2020, 3, 13) actual = get_start_datetime(api_token=api_token, since=since) - assert actual == expected + assert actual == expected def test_get_start_datetime_since_passed_with_no_response(monkeypatch): - def mock_get(*args, **kwargs): return MockResponseGetStartDateTime(404) - api_token = 'fake_api' + api_token = "fake_api" since = datetime.datetime.now() monkeypatch.setattr(requests, "get", mock_get) expected = datetime.date.today() actual = get_start_datetime(api_token=api_token, since=since) - assert actual == expected + assert actual == expected From dd0db66f793623320ddd32a05a754fd4e0e80bb7 Mon Sep 17 00:00:00 2001 From: Ryan Cheley Date: Mon, 10 Jan 2022 18:41:59 -0800 Subject: [PATCH 12/13] chore: added type checking --- justfile | 2 +- src/toggl_to_sqlite/utils.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/justfile b/justfile index c7061f3..184d4c6 100644 --- a/justfile +++ b/justfile @@ -23,7 +23,7 @@ gitclean: # run mypy on the files mypy: - mypy src/toggl_to_sqlite/*.py --no-strict-optional + mypy src/toggl_to_sqlite/*.py --no-strict-optional --ignore-missing-imports # generates the README.md file --help section diff --git a/src/toggl_to_sqlite/utils.py b/src/toggl_to_sqlite/utils.py index 5e9a99c..d8d726d 100644 --- a/src/toggl_to_sqlite/utils.py +++ b/src/toggl_to_sqlite/utils.py @@ -2,9 +2,10 @@ import math import requests +import sqlite_utils -def get_start_datetime(api_token, since: datetime = None): +def get_start_datetime(api_token: str, since: datetime.datetime = None) -> datetime.date: toggl = requests.get("https://api.track.toggl.com/api/v8/me", auth=(api_token, "api_token")) if toggl.status_code == 200: data = toggl.json() @@ -18,7 +19,7 @@ def get_start_datetime(api_token, since: datetime = None): return datetime.date.today() -def get_workspaces(api_token): +def get_workspaces(api_token: str) -> list: workspaces = [] response = requests.get("https://api.track.toggl.com/api/v8/workspaces", auth=(api_token, "api_token")) if response.status_code == 200: @@ -28,7 +29,7 @@ def get_workspaces(api_token): return workspaces -def get_projects(api_token): +def get_projects(api_token: str) -> list: projects = [] workspaces = get_workspaces(api_token) if len(workspaces) > 0: @@ -44,7 +45,7 @@ def get_projects(api_token): return projects -def get_time_entries(api_token, days, since: datetime = None): +def get_time_entries(api_token: str, days: int, since: datetime.datetime = None) -> list: start_date = get_start_datetime(api_token, since) today = datetime.date.today() data = [] @@ -67,7 +68,7 @@ def get_time_entries(api_token, days, since: datetime = None): return data -def save_items(items, table, db): +def save_items(items: list, table: str, db: sqlite_utils.Database) -> None: for item in items: data = item db[table].insert_all(data, pk="id", alter=True, replace=True) From c131b8593dbb1d8b6c1ea191ad519cc57834e996 Mon Sep 17 00:00:00 2001 From: Ryan Cheley Date: Mon, 10 Jan 2022 18:44:13 -0800 Subject: [PATCH 13/13] chore: added pre-commit to test requirements --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index baa7f24..c646ecc 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ def get_long_description(): toggl-to-sqlite=toggl_to_sqlite.cli:cli """, install_requires=["sqlite-utils>=2.4.4", "click", "requests", "requests_mock", "toml"], - extras_require={"test": ["pytest", "black", "isort", "coverage", "mypy", "cogapp"]}, + extras_require={"test": ["pytest", "black", "isort", "coverage", "mypy", "cogapp", "pre-commit"]}, tests_require=["toggl-to-sqlite[test]"], python_requires=">=3.6", classifiers=[