diff --git a/.github/workflows/deploy-documentation.yaml b/.github/workflows/deploy-documentation.yaml
new file mode 100644
index 00000000..a8d2e3cd
--- /dev/null
+++ b/.github/workflows/deploy-documentation.yaml
@@ -0,0 +1,35 @@
+name: Deploy
+
+on:
+ release:
+ types: [released]
+
+jobs:
+ deploy:
+ name: Deploy
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: Checkout the repository
+ uses: actions/checkout@v3
+
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: "3.9"
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements-dev.txt
+
+ - name: Build documentation
+ run: python scripts/gen-cli-docs.py
+
+ - name: Deploy docs
+ run: mkdocs gh-deploy --clean
+
+ - name: Deploy to PyPI
+ uses: pypa/gh-action-pypi-publish@master
+ with:
+ password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.github/workflows/linting-testing.yaml b/.github/workflows/linting-testing.yaml
new file mode 100644
index 00000000..ffa0e97a
--- /dev/null
+++ b/.github/workflows/linting-testing.yaml
@@ -0,0 +1,38 @@
+name: Linting and Testing
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+
+jobs:
+ linting-testing:
+ name: Linting and Testing
+ strategy:
+ matrix:
+ version: ["3.5", "3.6", "3.7", "3.8", "3.9"]
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: Checkout the repository
+ uses: actions/checkout@v3
+
+ - name: Setup Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements-dev.txt
+
+ - name: Run black
+ run: black --check .
+
+ - name: Run flake8
+ run: flake8 --show-source watson/ tests/ scripts/
+
+ - name: Run pytest
+ run: pytest tests
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 88bee9a6..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-language: python
-
-matrix:
- include:
- - python: 3.7
- os: linux
- dist: xenial
- env: TOXENV=flake8
- - python: 3.6
- os: linux
- dist: trusty
- env: TOXENV=py36
- - python: 3.7
- os: linux
- dist: xenial
- env: TOXENV=py37
- - python: 3.8
- os: linux
- dist: xenial
- env: TOXENV=py38
- - python: 3.9
- os: linux
- dist: xenial
- env: TOXENV=py39
-
-install:
- - pip install tox
-
-script:
- - tox -e "${TOXENV}"
diff --git a/MANIFEST.in b/MANIFEST.in
index 277ccae5..6550a625 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -6,7 +6,6 @@ include mkdocs.yml
include README.md
include requirements-dev.txt
include requirements.txt
-include tox.ini
include watson.completion
include watson.zsh-completion
diff --git a/docs/contributing/hack.md b/docs/contributing/hack.md
index d490bc1d..c306fa26 100644
--- a/docs/contributing/hack.md
+++ b/docs/contributing/hack.md
@@ -52,10 +52,9 @@ Ready to contribute? Here's how to set up *Watson* for local development.
inside the virtual environment. You can run `watson projects` to check
that your real projects are not shown.
-6. When you're done making changes, check that your changes pass the tests
- (see [Run the tests](#run-the-tests)):
+6. When you're done making changes, check that your changes pass the tests:
- (.venv) $ tox
+ (.venv) $ pytest tests
7. If you have added a new command or updated/fixed docstrings, please update
the documentation:
@@ -70,19 +69,3 @@ Ready to contribute? Here's how to set up *Watson* for local development.
9. After [reading this](./pr-guidelines.md), submit a pull request through the
GitHub website.
-
-
-## Run the tests
-
-The tests use [pytest](http://pytest.org/). To run them with the default Python
-interpreter:
-
- $ py.test -v tests/
-
-To run the tests via [tox](http://tox.testrun.org/) with all Python versions
-which are available on your system and are defined in the `tox.ini` file,
-simply run:
-
- $ tox
-
-This will also check the source code with [flake8](http://flake8.pycqa.org).
diff --git a/docs/contributing/pr-guidelines.md b/docs/contributing/pr-guidelines.md
index 1a42bdd7..e25b769e 100644
--- a/docs/contributing/pr-guidelines.md
+++ b/docs/contributing/pr-guidelines.md
@@ -3,9 +3,8 @@
> *nota bene*
>
> Open a pull-request even if your contribution is not ready yet! It can
-> be discussed and improved collaboratively! You may prefix the title of
-> your pull-request with "WIP: " to make it clear that it is not yet ready
-> for merging.
+> be discussed and improved collaboratively! You can create a draft PR to
+> indicate that it is not finished yet.
Before we merge a pull request, we check that it meets these guidelines:
@@ -24,7 +23,7 @@ Before we merge a pull request, we check that it meets these guidelines:
4. The pull request **should** include tests.
5. If the pull request adds functionality, the docs **should** be
updated.
-6. *TravisCI* integration tests should be **green** :) It will make
+6. GitHub linting and testing workflows should be **green** :) They will make
sure the tests pass with every supported version of Python.
Thank you!
diff --git a/docs/contributing/release-new-version.md b/docs/contributing/release-new-version.md
index e93f9ba3..898f45f8 100644
--- a/docs/contributing/release-new-version.md
+++ b/docs/contributing/release-new-version.md
@@ -32,36 +32,12 @@ $ git push origin --tags
Create a new pull request (PR) with the `prepare-x.y.z` branch. You can safely
merge this PR if all tests are green.
-Draft a new [Watson Release on
+Create and publish a new [Watson Release on
GitHub](https://github.com/TailorDev/Watson/releases) with the same release
notes.
-## Push the `x.y.z` release to PyPI
-
-Checkout the up-to-date `master` branch:
-
-```bash
-$ git checkout master
-$ git pull --rebase origin master
-```
-
-Now, build the release and submit it to PyPI using
-[twine](https://github.com/pypa/twine) (you'll need to be registered as a
-maintainer of the package):
-
-```bash
-$ python setup.py sdist bdist_wheel
-$ twine upload dist/*
-```
-
-## Update online documentation
-
-We use [`mkdocs`](http://www.mkdocs.org) to generate the online documentation.
-It must be updated via:
-
-```bash
-$ mkdocs gh-deploy --clean
-```
+A GitHub workflow will now automatically update the online documentation, and publish
+the release to PyPI.
## Publish the `x.y.z` release to Homebrew
diff --git a/pytest.ini b/pytest.ini
deleted file mode 100644
index 04a84cb1..00000000
--- a/pytest.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[pytest]
-markers =
- datafiles: pytest-datafiles plugin marker. This avoids warning message.
diff --git a/requirements-dev.txt b/requirements-dev.txt
index d9db059d..3f7a2443 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -10,9 +10,9 @@ twine
# Tests
flake8
+black
py
pytest
pytest-datafiles
pytest-mock
pytest-runner
-tox
diff --git a/scripts/fuzzer.py b/scripts/fuzzer.py
index 0b297938..687a3634 100755
--- a/scripts/fuzzer.py
+++ b/scripts/fuzzer.py
@@ -7,15 +7,13 @@
from watson import Watson
-if not os.environ.get('WATSON_DIR'):
+if not os.environ.get("WATSON_DIR"):
sys.exit(
"This script will corrupt Watson's data, please set the WATSON_DIR "
"environment variable to safely use it for development purpose."
)
-watson = Watson(config_dir=os.environ.get('WATSON_DIR'),
- frames=None,
- current=None)
+watson = Watson(config_dir=os.environ.get("WATSON_DIR"), frames=None, current=None)
projects = [
("apollo11", ["reactor", "module", "wheels", "steering", "brakes"]),
@@ -26,13 +24,14 @@
now = arrow.now()
-for date in arrow.Arrow.range('day', now.shift(months=-1), now):
+for date in arrow.Arrow.range("day", now.shift(months=-1), now):
if date.weekday() in (5, 6):
# Weekend \o/
continue
- start = date.replace(hour=9, minute=random.randint(0, 59)) \
- .shift(seconds=random.randint(0, 59))
+ start = date.replace(hour=9, minute=random.randint(0, 59)).shift(
+ seconds=random.randint(0, 59)
+ )
while start.hour < random.randint(16, 19):
project, tags = random.choice(projects)
@@ -40,7 +39,7 @@
project,
start,
start.shift(seconds=random.randint(60, 4 * 60 * 60)),
- tags=random.sample(tags, random.randint(0, len(tags)))
+ tags=random.sample(tags, random.randint(0, len(tags))),
)
start = frame.stop.shift(seconds=random.randint(0, 1 * 60 * 60))
diff --git a/scripts/gen-cli-docs.py b/scripts/gen-cli-docs.py
index 65ad9b79..06d603bd 100755
--- a/scripts/gen-cli-docs.py
+++ b/scripts/gen-cli-docs.py
@@ -5,44 +5,43 @@
from click.core import Command, Context
from click.formatting import HelpFormatter
from watson import cli as watson_cli
+
# from watson import watson
class MarkdownFormatter(HelpFormatter):
-
def write_heading(self, heading):
"""Writes a heading into the buffer."""
- self.write('### {}\n'.format(heading))
+ self.write("### {}\n".format(heading))
- def write_usage(self, prog, args='', prefix='Usage: '):
+ def write_usage(self, prog, args="", prefix="Usage: "):
"""Writes a usage line into the buffer.
:param prog: the program name.
:param args: whitespace separated list of arguments.
:param prefix: the prefix for the first line.
"""
- self.write('```bash\n{} {} {}\n```\n'.format(prefix, prog, args))
+ self.write("```bash\n{} {} {}\n```\n".format(prefix, prog, args))
def write_text(self, text):
- """Writes re-indented text into the buffer.
- """
+ """Writes re-indented text into the buffer."""
should_indent = False
rows = []
- for row in text.split('\n'):
+ for row in text.split("\n"):
if should_indent:
- row = ' {}'.format(row)
+ row = " {}".format(row)
- if '\b' in row:
- row = row.replace('\b', '', 1)
+ if "\b" in row:
+ row = row.replace("\b", "", 1)
should_indent = True
elif not len(row.strip()):
should_indent = False
rows.append(row)
- self.write("{}\n".format('\n'.join(rows)))
+ self.write("{}\n".format("\n".join(rows)))
def write_dl(self, rows, **kwargs):
"""Writes a definition list into the buffer. This is how options
@@ -50,22 +49,21 @@ def write_dl(self, rows, **kwargs):
:param rows: a list of two item tuples for the terms and values.
"""
rows = list(rows)
- self.write('\n')
+ self.write("\n")
- self.write('Flag | Help\n')
- self.write('-----|-----\n')
+ self.write("Flag | Help\n")
+ self.write("-----|-----\n")
for row in rows:
- self.write('`{}` | {}\n'.format(*row))
- self.write('\n')
+ self.write("`{}` | {}\n".format(*row))
+ self.write("\n")
class MkdocsContext(Context):
-
@property
def command_path(self):
# Not so proud of it
- return 'watson {}'.format(self.command.name)
+ return "watson {}".format(self.command.name)
def make_formatter(self):
return MarkdownFormatter()
@@ -84,18 +82,19 @@ def is_click_command(obj):
return True
return False
- content = '\n'.join((
- "",
- "",
- "# Commands",
- "",
- ))
+ content = "\n".join(
+ (
+ "",
+ "",
+ "# Commands",
+ "",
+ )
+ )
# Iterate over commands to build docs
for cmd_name, cmd in inspect.getmembers(watson_cli, is_click_command):
@@ -106,14 +105,14 @@ def is_click_command(obj):
# Each command is a section
content += "## `{}`\n\n".format(cmd_name)
- content += ''.join(formatter.buffer)
+ content += "".join(formatter.buffer)
# Write the commands documentation file
- with open(rowsput, 'w') as f:
+ with open(rowsput, "w") as f:
f.write(content)
-if __name__ == '__main__':
+if __name__ == "__main__":
- commands_md = 'docs/user-guide/commands.md'
+ commands_md = "docs/user-guide/commands.md"
main(commands_md)
diff --git a/setup.cfg b/setup.cfg
index d662cb96..23afa26a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -3,3 +3,11 @@ universal=1
[aliases]
test=pytest
+
+[tool:pytest]
+markers =
+ datafiles: pytest-datafiles plugin marker. This avoids warning message.
+
+[flake8]
+max-line-length = 88
+extend-ignore = E203
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 22e16370..ffd9d111 100644
--- a/setup.py
+++ b/setup.py
@@ -5,16 +5,16 @@
from setuptools import setup
-with open('README.rst') as f:
+with open("README.rst") as f:
readme = f.read()
# read package meta-data from version.py
pkg = {}
-mod = join('watson', 'version.py')
-exec(compile(open(mod).read(), mod, 'exec'), {}, pkg)
+mod = join("watson", "version.py")
+exec(compile(open(mod).read(), mod, "exec"), {}, pkg)
-def parse_requirements(requirements, ignore=('setuptools',)):
+def parse_requirements(requirements, ignore=("setuptools",)):
"""Read dependencies from requirements file (with version numbers if any)
Note: this implementation does not support requirements files with extra
@@ -24,10 +24,10 @@ def parse_requirements(requirements, ignore=('setuptools',)):
packages = set()
for line in f:
line = line.strip()
- if line.startswith(('#', '-r', '--')):
+ if line.startswith(("#", "-r", "--")):
continue
- if '#egg=' in line:
- line = line.split('#egg=')[1]
+ if "#egg=" in line:
+ line = line.split("#egg=")[1]
pkg = line.strip()
if pkg not in ignore:
packages.add(pkg)
@@ -35,21 +35,21 @@ def parse_requirements(requirements, ignore=('setuptools',)):
setup(
- name='td-watson',
- version=pkg['version'],
- description='A wonderful CLI to track your time!',
+ name="td-watson",
+ version=pkg["version"],
+ description="A wonderful CLI to track your time!",
url="https://github.com/TailorDev/Watson",
- packages=['watson'],
- author='TailorDev',
- author_email='contact@tailordev.fr',
- license='MIT',
+ packages=["watson"],
+ author="TailorDev",
+ author_email="contact@tailordev.fr",
+ license="MIT",
long_description=readme,
- install_requires=parse_requirements('requirements.txt'),
- python_requires='>=3.6',
- tests_require=parse_requirements('requirements-dev.txt'),
+ install_requires=parse_requirements("requirements.txt"),
+ python_requires=">=3.6",
+ tests_require=parse_requirements("requirements-dev.txt"),
entry_points={
- 'console_scripts': [
- 'watson = watson.__main__:cli',
+ "console_scripts": [
+ "watson = watson.__main__:cli",
]
},
classifiers=[
@@ -73,5 +73,5 @@ def parse_requirements(requirements, ignore=('setuptools',)):
"Topic :: Office/Business",
"Topic :: Utilities",
],
- keywords='watson time-tracking time tracking monitoring report',
+ keywords="watson time-tracking time tracking monitoring report",
)
diff --git a/tests/__init__.py b/tests/__init__.py
index 2c5f80ea..bbdd76c5 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -8,23 +8,18 @@
import py
-TEST_FIXTURE_DIR = py.path.local(
- os.path.dirname(
- os.path.realpath(__file__)
- )
- ) / 'resources'
+TEST_FIXTURE_DIR = (
+ py.path.local(os.path.dirname(os.path.realpath(__file__))) / "resources"
+)
def mock_datetime(dt, dt_module):
-
class DateTimeMeta(type):
-
@classmethod
def __instancecheck__(mcs, obj):
return isinstance(obj, datetime.datetime)
class BaseMockedDateTime(datetime.datetime):
-
@classmethod
def now(cls, tz=None):
return dt.replace(tzinfo=tz)
@@ -37,9 +32,9 @@ def utcnow(cls):
def today(cls):
return dt
- MockedDateTime = DateTimeMeta('datetime', (BaseMockedDateTime,), {})
+ MockedDateTime = DateTimeMeta("datetime", (BaseMockedDateTime,), {})
- return mock.patch.object(dt_module, 'datetime', MockedDateTime)
+ return mock.patch.object(dt_module, "datetime", MockedDateTime)
def mock_read(content):
diff --git a/tests/conftest.py b/tests/conftest.py
index 6f21a4bf..f8293602 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -8,7 +8,7 @@
@pytest.fixture
def config_dir(tmpdir):
- return str(tmpdir.mkdir('config'))
+ return str(tmpdir.mkdir("config"))
@pytest.fixture
diff --git a/tests/test_autocompletion.py b/tests/test_autocompletion.py
index fb5b3ad8..bc76df3d 100644
--- a/tests/test_autocompletion.py
+++ b/tests/test_autocompletion.py
@@ -42,9 +42,7 @@
(get_tags, None, []),
],
)
-def test_if_returned_values_are_distinct(
- watson_df, func_to_test, rename_type, args
-):
+def test_if_returned_values_are_distinct(watson_df, func_to_test, rename_type, args):
ctx = ClickContext(obj=watson_df, params={"rename_type": rename_type})
prefix = ""
ret_list = list(func_to_test(ctx, args, prefix))
@@ -89,9 +87,7 @@ def test_if_empty_prefix_returns_everything(
(get_tags, None, []),
],
)
-def test_completion_of_nonexisting_prefix(
- watson_df, func_to_test, rename_type, args
-):
+def test_completion_of_nonexisting_prefix(watson_df, func_to_test, rename_type, args):
ctx = ClickContext(obj=watson_df, params={"rename_type": rename_type})
prefix = "NOT-EXISTING-PREFIX"
ret_list = list(func_to_test(ctx, args, prefix))
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 2a1b3a52..5404f04a 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -11,204 +11,198 @@
# Not all ISO-8601 compliant strings are recognized by arrow.get(str)
VALID_DATES_DATA = [
- ('2018', '2018-01-01 00:00:00'), # years
- ('2018-04', '2018-04-01 00:00:00'), # calendar dates
- ('2018-04-10', '2018-04-10 00:00:00'),
- ('2018/04/10', '2018-04-10 00:00:00'),
- ('2018.04.10', '2018-04-10 00:00:00'),
- ('2018-4-10', '2018-04-10 00:00:00'),
- ('2018/4/10', '2018-04-10 00:00:00'),
- ('2018.4.10', '2018-04-10 00:00:00'),
- ('20180410', '2018-04-10 00:00:00'),
- ('2018-123', '2018-05-03 00:00:00'), # ordinal dates
- ('2018-04-10 12:30:43', '2018-04-10 12:30:43'),
- ('2018-04-10T12:30:43', '2018-04-10 12:30:43'),
- ('2018-04-10 12:30:43Z', '2018-04-10 12:30:43'),
- ('2018-04-10 12:30:43.1233', '2018-04-10 12:30:43'),
- ('2018-04-10 12:30:43+03:00', '2018-04-10 12:30:43'),
- ('2018-04-10 12:30:43-07:00', '2018-04-10 12:30:43'),
- ('2018-04-10T12:30:43-07:00', '2018-04-10 12:30:43'),
- ('2018-04-10 12:30', '2018-04-10 12:30:00'),
- ('2018-04-10T12:30', '2018-04-10 12:30:00'),
- ('2018-04-10 12', '2018-04-10 12:00:00'),
- ('2018-04-10T12', '2018-04-10 12:00:00'),
+ ("2018", "2018-01-01 00:00:00"), # years
+ ("2018-04", "2018-04-01 00:00:00"), # calendar dates
+ ("2018-04-10", "2018-04-10 00:00:00"),
+ ("2018/04/10", "2018-04-10 00:00:00"),
+ ("2018.04.10", "2018-04-10 00:00:00"),
+ ("2018-4-10", "2018-04-10 00:00:00"),
+ ("2018/4/10", "2018-04-10 00:00:00"),
+ ("2018.4.10", "2018-04-10 00:00:00"),
+ ("20180410", "2018-04-10 00:00:00"),
+ ("2018-123", "2018-05-03 00:00:00"), # ordinal dates
+ ("2018-04-10 12:30:43", "2018-04-10 12:30:43"),
+ ("2018-04-10T12:30:43", "2018-04-10 12:30:43"),
+ ("2018-04-10 12:30:43Z", "2018-04-10 12:30:43"),
+ ("2018-04-10 12:30:43.1233", "2018-04-10 12:30:43"),
+ ("2018-04-10 12:30:43+03:00", "2018-04-10 12:30:43"),
+ ("2018-04-10 12:30:43-07:00", "2018-04-10 12:30:43"),
+ ("2018-04-10T12:30:43-07:00", "2018-04-10 12:30:43"),
+ ("2018-04-10 12:30", "2018-04-10 12:30:00"),
+ ("2018-04-10T12:30", "2018-04-10 12:30:00"),
+ ("2018-04-10 12", "2018-04-10 12:00:00"),
+ ("2018-04-10T12", "2018-04-10 12:00:00"),
(
- '14:05:12',
- arrow.now()
- .replace(hour=14, minute=5, second=12)
- .format('YYYY-MM-DD HH:mm:ss')
+ "14:05:12",
+ arrow.now().replace(hour=14, minute=5, second=12).format("YYYY-MM-DD HH:mm:ss"),
),
(
- '14:05',
- arrow.now()
- .replace(hour=14, minute=5, second=0)
- .format('YYYY-MM-DD HH:mm:ss')
+ "14:05",
+ arrow.now().replace(hour=14, minute=5, second=0).format("YYYY-MM-DD HH:mm:ss"),
),
- ('2018-W08', '2018-02-19 00:00:00'), # week dates
- ('2018W08', '2018-02-19 00:00:00'),
- ('2018-W08-2', '2018-02-20 00:00:00'),
- ('2018W082', '2018-02-20 00:00:00'),
+ ("2018-W08", "2018-02-19 00:00:00"), # week dates
+ ("2018W08", "2018-02-19 00:00:00"),
+ ("2018-W08-2", "2018-02-20 00:00:00"),
+ ("2018W082", "2018-02-20 00:00:00"),
]
INVALID_DATES_DATA = [
- (' 2018'),
- ('2018 '),
- ('201804'),
- ('18-04-10'),
- ('180410'), # truncated representation not allowed
- ('hello 2018'),
- ('yesterday'),
- ('tomorrow'),
- ('14:05:12.000'), # Times alone are not allowed
- ('140512.000'),
- ('140512'),
- ('14.05'),
- ('2018-04-10T'),
- ('2018-04-10T12:30:43.'),
+ (" 2018"),
+ ("2018 "),
+ ("201804"),
+ ("18-04-10"),
+ ("180410"), # truncated representation not allowed
+ ("hello 2018"),
+ ("yesterday"),
+ ("tomorrow"),
+ ("14:05:12.000"), # Times alone are not allowed
+ ("140512.000"),
+ ("140512"),
+ ("14.05"),
+ ("2018-04-10T"),
+ ("2018-04-10T12:30:43."),
]
VALID_TIMES_DATA = [
- ('14:12'),
- ('14:12:43'),
- ('2019-04-10T14:12'),
- ('2019-04-10T14:12:43'),
+ ("14:12"),
+ ("14:12:43"),
+ ("2019-04-10T14:12"),
+ ("2019-04-10T14:12:43"),
]
class OutputParser:
- FRAME_ID_PATTERN = re.compile(r'id: (?P[0-9a-f]+)')
+ FRAME_ID_PATTERN = re.compile(r"id: (?P[0-9a-f]+)")
@staticmethod
def get_frame_id(output):
- return OutputParser.FRAME_ID_PATTERN.search(output).group('frame_id')
+ return OutputParser.FRAME_ID_PATTERN.search(output).group("frame_id")
@staticmethod
def get_start_date(watson, output):
frame_id = OutputParser.get_frame_id(output)
- return watson.frames[frame_id].start.format('YYYY-MM-DD HH:mm:ss')
+ return watson.frames[frame_id].start.format("YYYY-MM-DD HH:mm:ss")
# watson add
-@pytest.mark.parametrize('test_dt,expected', VALID_DATES_DATA)
+
+@pytest.mark.parametrize("test_dt,expected", VALID_DATES_DATA)
def test_add_valid_date(runner, watson, test_dt, expected):
result = runner.invoke(
- cli.add,
- ['-f', test_dt, '-t', test_dt, 'project-name'],
- obj=watson)
+ cli.add, ["-f", test_dt, "-t", test_dt, "project-name"], obj=watson
+ )
assert result.exit_code == 0
assert OutputParser.get_start_date(watson, result.output) == expected
-@pytest.mark.parametrize('test_dt', INVALID_DATES_DATA)
+@pytest.mark.parametrize("test_dt", INVALID_DATES_DATA)
def test_add_invalid_date(runner, watson, test_dt):
- result = runner.invoke(cli.add,
- ['-f', test_dt, '-t', test_dt, 'project-name'],
- obj=watson)
+ result = runner.invoke(
+ cli.add, ["-f", test_dt, "-t", test_dt, "project-name"], obj=watson
+ )
assert result.exit_code != 0
# watson aggregate
-@pytest.mark.parametrize('test_dt,expected', VALID_DATES_DATA)
+
+@pytest.mark.parametrize("test_dt,expected", VALID_DATES_DATA)
def test_aggregate_valid_date(runner, watson, test_dt, expected):
# This is super fast, because no internal 'report' invocations are made
- result = runner.invoke(cli.aggregate,
- ['-f', test_dt, '-t', test_dt],
- obj=watson)
+ result = runner.invoke(cli.aggregate, ["-f", test_dt, "-t", test_dt], obj=watson)
assert result.exit_code == 0
-@pytest.mark.parametrize('test_dt', INVALID_DATES_DATA)
+@pytest.mark.parametrize("test_dt", INVALID_DATES_DATA)
def test_aggregate_invalid_date(runner, watson, test_dt):
# This is super fast, because no internal 'report' invocations are made
- result = runner.invoke(cli.aggregate,
- ['-f', test_dt, '-t', test_dt],
- obj=watson)
+ result = runner.invoke(cli.aggregate, ["-f", test_dt, "-t", test_dt], obj=watson)
assert result.exit_code != 0
# watson log
-@pytest.mark.parametrize('cmd', [cli.aggregate, cli.log, cli.report])
+
+@pytest.mark.parametrize("cmd", [cli.aggregate, cli.log, cli.report])
def test_incompatible_options(runner, watson, cmd):
- name_interval_options = ['--' + s for s in cli._SHORTCUT_OPTIONS]
+ name_interval_options = ["--" + s for s in cli._SHORTCUT_OPTIONS]
for opt1, opt2 in combinations(name_interval_options, 2):
result = runner.invoke(cmd, [opt1, opt2], obj=watson)
assert result.exit_code != 0
-@pytest.mark.parametrize('test_dt,expected', VALID_DATES_DATA)
+@pytest.mark.parametrize("test_dt,expected", VALID_DATES_DATA)
def test_log_valid_date(runner, watson, test_dt, expected):
- result = runner.invoke(cli.log, ['-f', test_dt, '-t', test_dt], obj=watson)
+ result = runner.invoke(cli.log, ["-f", test_dt, "-t", test_dt], obj=watson)
assert result.exit_code == 0
-@pytest.mark.parametrize('test_dt', INVALID_DATES_DATA)
+@pytest.mark.parametrize("test_dt", INVALID_DATES_DATA)
def test_log_invalid_date(runner, watson, test_dt):
- result = runner.invoke(cli.log, ['-f', test_dt, '-t', test_dt], obj=watson)
+ result = runner.invoke(cli.log, ["-f", test_dt, "-t", test_dt], obj=watson)
assert result.exit_code != 0
# watson report
-@pytest.mark.parametrize('test_dt,expected', VALID_DATES_DATA)
+
+@pytest.mark.parametrize("test_dt,expected", VALID_DATES_DATA)
def test_report_valid_date(runner, watson, test_dt, expected):
- result = runner.invoke(cli.report,
- ['-f', test_dt, '-t', test_dt],
- obj=watson)
+ result = runner.invoke(cli.report, ["-f", test_dt, "-t", test_dt], obj=watson)
assert result.exit_code == 0
-@pytest.mark.parametrize('test_dt', INVALID_DATES_DATA)
+@pytest.mark.parametrize("test_dt", INVALID_DATES_DATA)
def test_report_invalid_date(runner, watson, test_dt):
- result = runner.invoke(cli.report,
- ['-f', test_dt, '-t', test_dt],
- obj=watson)
+ result = runner.invoke(cli.report, ["-f", test_dt, "-t", test_dt], obj=watson)
assert result.exit_code != 0
# watson stop
-@pytest.mark.parametrize('at_dt', VALID_TIMES_DATA)
+
+@pytest.mark.parametrize("at_dt", VALID_TIMES_DATA)
def test_stop_valid_time(runner, watson, mocker, at_dt):
- mocker.patch('arrow.arrow.dt_datetime', wraps=datetime)
+ mocker.patch("arrow.arrow.dt_datetime", wraps=datetime)
start_dt = datetime(2019, 4, 10, 14, 0, 0, tzinfo=local_tz_info())
arrow.arrow.dt_datetime.now.return_value = start_dt
- result = runner.invoke(cli.start, ['a-project'], obj=watson)
+ result = runner.invoke(cli.start, ["a-project"], obj=watson)
assert result.exit_code == 0
# Simulate one hour has elapsed, so that 'at_dt' is older than now()
# but newer than the start date.
- arrow.arrow.dt_datetime.now.return_value = (start_dt + timedelta(hours=1))
- result = runner.invoke(cli.stop, ['--at', at_dt], obj=watson)
+ arrow.arrow.dt_datetime.now.return_value = start_dt + timedelta(hours=1)
+ result = runner.invoke(cli.stop, ["--at", at_dt], obj=watson)
assert result.exit_code == 0
# watson start
-@pytest.mark.parametrize('at_dt', VALID_TIMES_DATA)
+
+@pytest.mark.parametrize("at_dt", VALID_TIMES_DATA)
def test_start_valid_time(runner, watson, mocker, at_dt):
# Simulate a start date so that 'at_dt' is older than now().
- mocker.patch('arrow.arrow.dt_datetime', wraps=datetime)
+ mocker.patch("arrow.arrow.dt_datetime", wraps=datetime)
start_dt = datetime(2019, 4, 10, 14, 0, 0, tzinfo=local_tz_info())
- arrow.arrow.dt_datetime.now.return_value = (start_dt + timedelta(hours=1))
- result = runner.invoke(cli.start, ['a-project', '--at', at_dt], obj=watson)
+ arrow.arrow.dt_datetime.now.return_value = start_dt + timedelta(hours=1)
+ result = runner.invoke(cli.start, ["a-project", "--at", at_dt], obj=watson)
assert result.exit_code == 0
# watson restart
-@pytest.mark.parametrize('at_dt', VALID_TIMES_DATA)
+
+@pytest.mark.parametrize("at_dt", VALID_TIMES_DATA)
def test_restart_valid_time(runner, watson, mocker, at_dt):
# Create a previous entry the same as in `test_stop_valid_time`
- mocker.patch('arrow.arrow.dt_datetime', wraps=datetime)
+ mocker.patch("arrow.arrow.dt_datetime", wraps=datetime)
start_dt = datetime(2019, 4, 10, 14, 0, 0, tzinfo=local_tz_info())
arrow.arrow.dt_datetime.now.return_value = start_dt
- result = runner.invoke(cli.start, ['a-project'], obj=watson)
+ result = runner.invoke(cli.start, ["a-project"], obj=watson)
# Simulate one hour has elapsed, so that 'at_dt' is older than now()
# but newer than the start date.
- arrow.arrow.dt_datetime.now.return_value = (start_dt + timedelta(hours=1))
- result = runner.invoke(cli.stop, ['--at', at_dt], obj=watson)
+ arrow.arrow.dt_datetime.now.return_value = start_dt + timedelta(hours=1)
+ result = runner.invoke(cli.stop, ["--at", at_dt], obj=watson)
# Test that the last frame can be restarted
- result = runner.invoke(cli.restart, ['--at', at_dt], obj=watson)
+ result = runner.invoke(cli.restart, ["--at", at_dt], obj=watson)
assert result.exit_code == 0
diff --git a/tests/test_config.py b/tests/test_config.py
index 8bdb04ed..eb46cb11 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -13,14 +13,14 @@ def test_config_get(mocker, watson):
url = foo
token =
"""
- mocker.patch.object(ConfigParser, 'read', mock_read(content))
+ mocker.patch.object(ConfigParser, "read", mock_read(content))
config = watson.config
- assert config.get('backend', 'url') == 'foo'
- assert config.get('backend', 'token') == ''
- assert config.get('backend', 'foo') is None
- assert config.get('backend', 'foo', 'bar') == 'bar'
- assert config.get('option', 'spamm') is None
- assert config.get('option', 'spamm', 'eggs') == 'eggs'
+ assert config.get("backend", "url") == "foo"
+ assert config.get("backend", "token") == ""
+ assert config.get("backend", "foo") is None
+ assert config.get("backend", "foo", "bar") == "bar"
+ assert config.get("option", "spamm") is None
+ assert config.get("option", "spamm", "eggs") == "eggs"
def test_config_getboolean(mocker, watson):
@@ -33,18 +33,18 @@ def test_config_getboolean(mocker, watson):
flag5 = false
flag6 =
"""
- mocker.patch.object(ConfigParser, 'read', mock_read(content))
+ mocker.patch.object(ConfigParser, "read", mock_read(content))
config = watson.config
- assert config.getboolean('options', 'flag1') is True
- assert config.getboolean('options', 'flag1', False) is True
- assert config.getboolean('options', 'flag2') is True
- assert config.getboolean('options', 'flag3') is True
- assert config.getboolean('options', 'flag4') is True
- assert config.getboolean('options', 'flag5') is False
- assert config.getboolean('options', 'flag6') is False
- assert config.getboolean('options', 'flag6', True) is True
- assert config.getboolean('options', 'missing') is False
- assert config.getboolean('options', 'missing', True) is True
+ assert config.getboolean("options", "flag1") is True
+ assert config.getboolean("options", "flag1", False) is True
+ assert config.getboolean("options", "flag2") is True
+ assert config.getboolean("options", "flag3") is True
+ assert config.getboolean("options", "flag4") is True
+ assert config.getboolean("options", "flag5") is False
+ assert config.getboolean("options", "flag6") is False
+ assert config.getboolean("options", "flag6", True) is True
+ assert config.getboolean("options", "missing") is False
+ assert config.getboolean("options", "missing", True) is True
def test_config_getint(mocker, watson):
@@ -54,21 +54,21 @@ def test_config_getint(mocker, watson):
value2 = spamm
value3 =
"""
- mocker.patch.object(ConfigParser, 'read', mock_read(content))
+ mocker.patch.object(ConfigParser, "read", mock_read(content))
config = watson.config
- assert config.getint('options', 'value1') == 42
- assert config.getint('options', 'value1', 666) == 42
- assert config.getint('options', 'missing') is None
- assert config.getint('options', 'missing', 23) == 23
+ assert config.getint("options", "value1") == 42
+ assert config.getint("options", "value1", 666) == 42
+ assert config.getint("options", "missing") is None
+ assert config.getint("options", "missing", 23) == 23
# default is not converted!
- assert config.getint('options', 'missing', '42') == '42'
- assert config.getint('options', 'missing', 6.66) == 6.66
+ assert config.getint("options", "missing", "42") == "42"
+ assert config.getint("options", "missing", 6.66) == 6.66
with pytest.raises(ValueError):
- config.getint('options', 'value2')
+ config.getint("options", "value2")
with pytest.raises(ValueError):
- config.getint('options', 'value3')
+ config.getint("options", "value3")
def test_config_getfloat(mocker, watson):
@@ -80,22 +80,22 @@ def test_config_getfloat(mocker, watson):
value4 =
"""
- mocker.patch.object(ConfigParser, 'read', mock_read(content))
+ mocker.patch.object(ConfigParser, "read", mock_read(content))
config = watson.config
- assert config.getfloat('options', 'value1') == 3.14
- assert config.getfloat('options', 'value1', 6.66) == 3.14
- assert config.getfloat('options', 'value2') == 42.0
- assert isinstance(config.getfloat('options', 'value2'), float)
- assert config.getfloat('options', 'missing') is None
- assert config.getfloat('options', 'missing', 3.14) == 3.14
+ assert config.getfloat("options", "value1") == 3.14
+ assert config.getfloat("options", "value1", 6.66) == 3.14
+ assert config.getfloat("options", "value2") == 42.0
+ assert isinstance(config.getfloat("options", "value2"), float)
+ assert config.getfloat("options", "missing") is None
+ assert config.getfloat("options", "missing", 3.14) == 3.14
# default is not converted!
- assert config.getfloat('options', 'missing', '3.14') == '3.14'
+ assert config.getfloat("options", "missing", "3.14") == "3.14"
with pytest.raises(ValueError):
- config.getfloat('options', 'value3')
+ config.getfloat("options", "value3")
with pytest.raises(ValueError):
- config.getfloat('options', 'value4')
+ config.getfloat("options", "value4")
def test_config_getlist(mocker, watson):
@@ -121,33 +121,32 @@ def test_config_getlist(mocker, watson):
two #three
four # five
"""
- mocker.patch.object(ConfigParser, 'read', mock_read(content))
+ mocker.patch.object(ConfigParser, "read", mock_read(content))
gl = watson.config.getlist
- assert gl('options', 'value1') == ['one', 'two three', 'four',
- 'five six']
- assert gl('options', 'value2') == ['one', 'two three', 'four',
- 'five six']
- assert gl('options', 'value3') == ['one', 'two three']
- assert gl('options', 'value4') == ['one', 'two three', 'four']
- assert gl('options', 'value5') == ['one', 'two #three', 'four # five']
+ assert gl("options", "value1") == ["one", "two three", "four", "five six"]
+ assert gl("options", "value2") == ["one", "two three", "four", "five six"]
+ assert gl("options", "value3") == ["one", "two three"]
+ assert gl("options", "value4") == ["one", "two three", "four"]
+ assert gl("options", "value5") == ["one", "two #three", "four # five"]
# default values
- assert gl('options', 'novalue') == []
- assert gl('options', 'novalue', None) == []
- assert gl('options', 'novalue', 42) == 42
- assert gl('nosection', 'dummy') == []
- assert gl('nosection', 'dummy', None) == []
- assert gl('nosection', 'dummy', 42) == 42
-
- default = gl('nosection', 'dummy')
+ assert gl("options", "novalue") == []
+ assert gl("options", "novalue", None) == []
+ assert gl("options", "novalue", 42) == 42
+ assert gl("nosection", "dummy") == []
+ assert gl("nosection", "dummy", None) == []
+ assert gl("nosection", "dummy", 42) == 42
+
+ default = gl("nosection", "dummy")
default.append(42)
- assert gl('nosection', 'dummy') != [42], (
- "Modifying default return value should not have side effect.")
+ assert gl("nosection", "dummy") != [
+ 42
+ ], "Modifying default return value should not have side effect."
def test_set_config(watson):
config = ConfigParser()
- config.set('foo', 'bar', 'lol')
+ config.set("foo", "bar", "lol")
watson.config = config
- assert watson.config.get('foo', 'bar') == 'lol'
+ assert watson.config.get("foo", "bar") == "lol"
diff --git a/tests/test_utils.py b/tests/test_utils.py
index b993767f..7a032b36 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -35,36 +35,41 @@
_dt = functools.partial(datetime.datetime, tzinfo=tzutc())
-@pytest.mark.parametrize('now, mode, start_time', [
- (_dt(2016, 6, 2), 'year', _dt(2016, 1, 1)),
- (_dt(2016, 6, 2), 'month', _dt(2016, 6, 1)),
- (_dt(2016, 6, 2), 'week', _dt(2016, 5, 30)),
- (_dt(2016, 6, 2), 'day', _dt(2016, 6, 2)),
- (_dt(2016, 6, 2), 'all', _dt(1970, 1, 1)),
- (_dt(2016, 6, 2), 'luna', _dt(2016, 5, 21, 21, 16)),
-
- (_dt(2012, 2, 24), 'year', _dt(2012, 1, 1)),
- (_dt(2012, 2, 24), 'month', _dt(2012, 2, 1)),
- (_dt(2012, 2, 24), 'week', _dt(2012, 2, 20)),
- (_dt(2012, 2, 24), 'day', _dt(2012, 2, 24)),
- (_dt(2012, 2, 24), 'all', _dt(1970, 1, 1)),
- (_dt(2012, 2, 24), 'luna', _dt(2012, 2, 7, 21, 56)),
-])
+@pytest.mark.parametrize(
+ "now, mode, start_time",
+ [
+ (_dt(2016, 6, 2), "year", _dt(2016, 1, 1)),
+ (_dt(2016, 6, 2), "month", _dt(2016, 6, 1)),
+ (_dt(2016, 6, 2), "week", _dt(2016, 5, 30)),
+ (_dt(2016, 6, 2), "day", _dt(2016, 6, 2)),
+ (_dt(2016, 6, 2), "all", _dt(1970, 1, 1)),
+ (_dt(2016, 6, 2), "luna", _dt(2016, 5, 21, 21, 16)),
+ (_dt(2012, 2, 24), "year", _dt(2012, 1, 1)),
+ (_dt(2012, 2, 24), "month", _dt(2012, 2, 1)),
+ (_dt(2012, 2, 24), "week", _dt(2012, 2, 20)),
+ (_dt(2012, 2, 24), "day", _dt(2012, 2, 24)),
+ (_dt(2012, 2, 24), "all", _dt(1970, 1, 1)),
+ (_dt(2012, 2, 24), "luna", _dt(2012, 2, 7, 21, 56)),
+ ],
+)
def test_get_start_time_for_period(now, mode, start_time):
with mock_datetime(now, datetime):
assert get_start_time_for_period(mode).datetime == start_time
-@pytest.mark.parametrize("monday_start, week_start, new_start", [
- ("2018 12 3", "monday", "2018 12 3"),
- ("2018 12 3", "tuesday", "2018 12 4"),
- ("2018 12 3", "wednesday", "2018 12 5"),
- ("2018 12 3", "thursday", "2018 12 6"),
- ("2018 12 3", "friday", "2018 11 30"),
- ("2018 12 3", "saturday", "2018 12 1"),
- ("2018 12 3", "sunday", "2018 12 2"),
- ("2018 12 3", "typo", "2018 12 3"),
-])
+@pytest.mark.parametrize(
+ "monday_start, week_start, new_start",
+ [
+ ("2018 12 3", "monday", "2018 12 3"),
+ ("2018 12 3", "tuesday", "2018 12 4"),
+ ("2018 12 3", "wednesday", "2018 12 5"),
+ ("2018 12 3", "thursday", "2018 12 6"),
+ ("2018 12 3", "friday", "2018 11 30"),
+ ("2018 12 3", "saturday", "2018 12 1"),
+ ("2018 12 3", "sunday", "2018 12 2"),
+ ("2018 12 3", "typo", "2018 12 3"),
+ ],
+)
def test_apply_weekday_offset(monday_start, week_start, new_start):
with mock_datetime(_dt(2018, 12, 6), datetime):
original_start = arrow.get(monday_start, "YYYY MM D")
@@ -74,36 +79,36 @@ def test_apply_weekday_offset(monday_start, week_start, new_start):
def test_make_json_writer():
fp = StringIO()
- writer = make_json_writer(lambda: {'foo': 42})
+ writer = make_json_writer(lambda: {"foo": 42})
writer(fp)
assert fp.getvalue() == '{\n "foo": 42\n}'
def test_make_json_writer_with_args():
fp = StringIO()
- writer = make_json_writer(lambda x: {'foo': x}, 23)
+ writer = make_json_writer(lambda x: {"foo": x}, 23)
writer(fp)
assert fp.getvalue() == '{\n "foo": 23\n}'
def test_make_json_writer_with_kwargs():
fp = StringIO()
- writer = make_json_writer(lambda foo=None: {'foo': foo}, foo='bar')
+ writer = make_json_writer(lambda foo=None: {"foo": foo}, foo="bar")
writer(fp)
assert fp.getvalue() == '{\n "foo": "bar"\n}'
def test_make_json_writer_with_unicode():
fp = StringIO()
- writer = make_json_writer(lambda: {'ùñï©ôð€': 'εvεrywhεrε'})
+ writer = make_json_writer(lambda: {"ùñï©ôð€": "εvεrywhεrε"})
writer(fp)
expected = '{\n "ùñï©ôð€": "εvεrywhεrε"\n}'
assert fp.getvalue() == expected
def test_safe_save(config_dir):
- save_file = os.path.join(config_dir, 'test')
- backup_file = os.path.join(config_dir, 'test' + '.bak')
+ save_file = os.path.join(config_dir, "test")
+ backup_file = os.path.join(config_dir, "test" + ".bak")
assert not os.path.exists(save_file)
safe_save(save_file, lambda f: f.write("Success"))
@@ -126,8 +131,8 @@ def test_safe_save(config_dir):
def test_safe_save_tmpfile_on_other_filesystem(config_dir, mocker):
- save_file = os.path.join(config_dir, 'test')
- backup_file = os.path.join(config_dir, 'test' + '.bak')
+ save_file = os.path.join(config_dir, "test")
+ backup_file = os.path.join(config_dir, "test" + ".bak")
assert not os.path.exists(save_file)
safe_save(save_file, lambda f: f.write("Success"))
@@ -139,7 +144,7 @@ def test_safe_save_tmpfile_on_other_filesystem(config_dir, mocker):
# simulate tmpfile being on another file-system
# OSError is caught and handled by shutil.move() used by save_safe()
- mocker.patch('os.rename', side_effect=OSError)
+ mocker.patch("os.rename", side_effect=OSError)
safe_save(save_file, "Again")
assert os.path.exists(backup_file)
@@ -148,8 +153,8 @@ def test_safe_save_tmpfile_on_other_filesystem(config_dir, mocker):
def test_safe_save_with_exception(config_dir):
- save_file = os.path.join(config_dir, 'test')
- backup_file = os.path.join(config_dir, 'test' + '.bak')
+ save_file = os.path.join(config_dir, "test")
+ backup_file = os.path.join(config_dir, "test" + ".bak")
def failing_writer(f):
raise RuntimeError("Save failed.")
@@ -175,164 +180,175 @@ def failing_writer(f):
assert not os.path.exists(backup_file)
-@pytest.mark.parametrize('args, parsed_tags', [
- (['+ham', '+n', '+eggs'], ['ham', 'n', 'eggs']),
- (['+ham', 'n', '+eggs'], ['ham n', 'eggs']),
- (['ham', 'n', '+eggs'], ['eggs']),
- (['ham', '+n', 'eggs'], ['n eggs']),
- (['+ham', 'n', 'eggs'], ['ham n eggs']),
-])
+@pytest.mark.parametrize(
+ "args, parsed_tags",
+ [
+ (["+ham", "+n", "+eggs"], ["ham", "n", "eggs"]),
+ (["+ham", "n", "+eggs"], ["ham n", "eggs"]),
+ (["ham", "n", "+eggs"], ["eggs"]),
+ (["ham", "+n", "eggs"], ["n eggs"]),
+ (["+ham", "n", "eggs"], ["ham n eggs"]),
+ ],
+)
def test_parse_tags(args, parsed_tags):
tags = parse_tags(args)
assert tags == parsed_tags
def test_confirm_project_existing_project_returns_true():
- project = 'foo'
- watson_projects = ['foo', 'bar']
+ project = "foo"
+ watson_projects = ["foo", "bar"]
assert confirm_project(project, watson_projects)
-@patch('click.confirm', return_value=True)
+@patch("click.confirm", return_value=True)
def test_confirm_project_accept_returns_true(confirm):
- project = 'baz'
- watson_projects = ['foo', 'bar']
+ project = "baz"
+ watson_projects = ["foo", "bar"]
assert confirm_project(project, watson_projects)
-@patch('watson.utils.click.confirm', side_effect=Abort)
+@patch("watson.utils.click.confirm", side_effect=Abort)
def test_confirm_project_reject_raises_abort(confirm):
- project = 'baz'
- watson_projects = ['foo', 'bar']
+ project = "baz"
+ watson_projects = ["foo", "bar"]
with pytest.raises(Abort):
confirm_project(project, watson_projects)
def test_confirm_tags_existing_tag_returns_true():
- tags = ['a']
- watson_tags = ['a', 'b']
+ tags = ["a"]
+ watson_tags = ["a", "b"]
assert confirm_tags(tags, watson_tags)
-@patch('click.confirm', return_value=True)
+@patch("click.confirm", return_value=True)
def test_confirm_tags_accept_returns_true(confirm):
- tags = ['c']
- watson_tags = ['a', 'b']
+ tags = ["c"]
+ watson_tags = ["a", "b"]
assert confirm_tags(tags, watson_tags)
-@patch('click.confirm', side_effect=Abort)
+@patch("click.confirm", side_effect=Abort)
def test_confirm_tags_reject_raises_abort(confirm):
- tags = ['c']
- watson_tags = ['a', 'b']
+ tags = ["c"]
+ watson_tags = ["a", "b"]
with pytest.raises(Abort):
confirm_project(tags[0], watson_tags)
# build_csv
+
def test_build_csv_empty_data():
- assert build_csv([]) == ''
+ assert build_csv([]) == ""
def test_build_csv_one_col():
lt = os.linesep
- data = [{'col': 'value'}, {'col': 'another value'}]
- result = lt.join(['col', 'value', 'another value']) + lt
+ data = [{"col": "value"}, {"col": "another value"}]
+ result = lt.join(["col", "value", "another value"]) + lt
assert build_csv(data) == result
def test_build_csv_multiple_cols():
lt = os.linesep
- dm = csv.get_dialect('excel').delimiter
+ dm = csv.get_dialect("excel").delimiter
data = [
- co.OrderedDict([('col1', 'value'),
- ('col2', 'another value'),
- ('col3', 'more')]),
- co.OrderedDict([('col1', 'one value'),
- ('col2', 'two value'),
- ('col3', 'three')])
+ co.OrderedDict(
+ [("col1", "value"), ("col2", "another value"), ("col3", "more")]
+ ),
+ co.OrderedDict(
+ [("col1", "one value"), ("col2", "two value"), ("col3", "three")]
+ ),
]
- result = lt.join([
- dm.join(['col1', 'col2', 'col3']),
- dm.join(['value', 'another value', 'more']),
- dm.join(['one value', 'two value', 'three'])
- ]) + lt
+ result = (
+ lt.join(
+ [
+ dm.join(["col1", "col2", "col3"]),
+ dm.join(["value", "another value", "more"]),
+ dm.join(["one value", "two value", "three"]),
+ ]
+ )
+ + lt
+ )
assert build_csv(data) == result
# sorted_groupby
+
def test_sorted_groupby(watson):
end = arrow.utcnow()
- watson.add('foo', end.shift(hours=-25), end.shift(hours=-24), ['A'])
- watson.add('bar', end.shift(hours=-1), end, ['A'])
+ watson.add("foo", end.shift(hours=-25), end.shift(hours=-24), ["A"])
+ watson.add("bar", end.shift(hours=-1), end, ["A"])
- result = list(sorted_groupby(
- watson.frames,
- operator.attrgetter('day'),
- reverse=False))
+ result = list(
+ sorted_groupby(watson.frames, operator.attrgetter("day"), reverse=False)
+ )
assert result[0][0] < result[1][0]
def test_sorted_groupby_reverse(watson):
end = arrow.utcnow()
- watson.add('foo', end.shift(hours=-25), end.shift(hours=-24), ['A'])
- watson.add('bar', end.shift(hours=-1), end, ['A'])
+ watson.add("foo", end.shift(hours=-25), end.shift(hours=-24), ["A"])
+ watson.add("bar", end.shift(hours=-1), end, ["A"])
- result = list(sorted_groupby(
- watson.frames,
- operator.attrgetter('day'),
- reverse=True))
+ result = list(
+ sorted_groupby(watson.frames, operator.attrgetter("day"), reverse=True)
+ )
assert result[0][0] > result[1][0]
# frames_to_csv
+
def test_frames_to_csv_empty_data(watson):
- assert frames_to_csv(watson.frames) == ''
+ assert frames_to_csv(watson.frames) == ""
def test_frames_to_csv(watson):
- watson.start('foo', tags=['A', 'B'])
+ watson.start("foo", tags=["A", "B"])
watson.stop()
result = frames_to_csv(watson.frames)
read_csv = list(csv.reader(StringIO(result)))
- header = ['id', 'start', 'stop', 'project', 'tags']
+ header = ["id", "start", "stop", "project", "tags"]
assert len(read_csv) == 2
assert read_csv[0] == header
- assert read_csv[1][3] == 'foo'
- assert read_csv[1][4] == 'A, B'
+ assert read_csv[1][3] == "foo"
+ assert read_csv[1][4] == "A, B"
# frames_to_json
+
def test_frames_to_json_empty_data(watson):
- assert frames_to_json(watson.frames) == '[]'
+ assert frames_to_json(watson.frames) == "[]"
def test_frames_to_json(watson):
- watson.start('foo', tags=['A', 'B'])
+ watson.start("foo", tags=["A", "B"])
watson.stop()
result = json.loads(frames_to_json(watson.frames))
- keys = {'id', 'start', 'stop', 'project', 'tags'}
+ keys = {"id", "start", "stop", "project", "tags"}
assert len(result) == 1
assert set(result[0].keys()) == keys
- assert result[0]['project'] == 'foo'
- assert result[0]['tags'] == ['A', 'B']
+ assert result[0]["project"] == "foo"
+ assert result[0]["tags"] == ["A", "B"]
# flatten_report_for_csv
+
def test_flatten_report_for_csv(watson):
- now = arrow.utcnow().ceil('hour')
- watson.add('foo', now.shift(hours=-4), now, ['A', 'B'])
- watson.add('foo', now.shift(hours=-5), now.shift(hours=-4), ['A'])
- watson.add('foo', now.shift(hours=-7), now.shift(hours=-5), ['B'])
+ now = arrow.utcnow().ceil("hour")
+ watson.add("foo", now.shift(hours=-4), now, ["A", "B"])
+ watson.add("foo", now.shift(hours=-5), now.shift(hours=-4), ["A"])
+ watson.add("foo", now.shift(hours=-7), now.shift(hours=-5), ["B"])
start = now.shift(days=-1)
stop = now
@@ -340,23 +356,23 @@ def test_flatten_report_for_csv(watson):
assert len(result) == 3
- assert result[0]['from'] == start.format('YYYY-MM-DD 00:00:00')
- assert result[0]['to'] == stop.format('YYYY-MM-DD 23:59:59')
- assert result[0]['project'] == 'foo'
- assert result[0]['tag'] == ''
- assert result[0]['time'] == (4 + 1 + 2) * 3600
+ assert result[0]["from"] == start.format("YYYY-MM-DD 00:00:00")
+ assert result[0]["to"] == stop.format("YYYY-MM-DD 23:59:59")
+ assert result[0]["project"] == "foo"
+ assert result[0]["tag"] == ""
+ assert result[0]["time"] == (4 + 1 + 2) * 3600
- assert result[1]['from'] == start.format('YYYY-MM-DD 00:00:00')
- assert result[1]['to'] == stop.format('YYYY-MM-DD 23:59:59')
- assert result[1]['project'] == 'foo'
- assert result[1]['tag'] == 'A'
- assert result[1]['time'] == (4 + 1) * 3600
+ assert result[1]["from"] == start.format("YYYY-MM-DD 00:00:00")
+ assert result[1]["to"] == stop.format("YYYY-MM-DD 23:59:59")
+ assert result[1]["project"] == "foo"
+ assert result[1]["tag"] == "A"
+ assert result[1]["time"] == (4 + 1) * 3600
- assert result[2]['from'] == start.format('YYYY-MM-DD 00:00:00')
- assert result[2]['to'] == stop.format('YYYY-MM-DD 23:59:59')
- assert result[2]['project'] == 'foo'
- assert result[2]['tag'] == 'B'
- assert result[2]['time'] == (4 + 2) * 3600
+ assert result[2]["from"] == start.format("YYYY-MM-DD 00:00:00")
+ assert result[2]["to"] == stop.format("YYYY-MM-DD 23:59:59")
+ assert result[2]["project"] == "foo"
+ assert result[2]["tag"] == "B"
+ assert result[2]["time"] == (4 + 2) * 3600
def test_json_arrow_encoder():
@@ -364,7 +380,7 @@ def test_json_arrow_encoder():
json_arrow_encoder(0)
with pytest.raises(TypeError):
- json_arrow_encoder('foo')
+ json_arrow_encoder("foo")
with pytest.raises(TypeError):
json_arrow_encoder(None)
diff --git a/tests/test_watson.py b/tests/test_watson.py
index b44ee172..958ac2fd 100644
--- a/tests/test_watson.py
+++ b/tests/test_watson.py
@@ -17,9 +17,7 @@
@pytest.fixture
def json_mock(mocker):
- return mocker.patch.object(
- json, 'dumps', side_effect=json.dumps, autospec=True
- )
+ return mocker.patch.object(json, "dumps", side_effect=json.dumps, autospec=True)
# NOTE: All timestamps need to be > 3600 to avoid breaking the tests on
@@ -27,49 +25,49 @@ def json_mock(mocker):
# current
+
def test_current(mocker, watson):
- content = json.dumps({'project': 'foo', 'start': 4000, 'tags': ['A', 'B']})
+ content = json.dumps({"project": "foo", "start": 4000, "tags": ["A", "B"]})
- mocker.patch('builtins.open', mocker.mock_open(read_data=content))
- assert watson.current['project'] == 'foo'
- assert watson.current['start'] == arrow.get(4000)
- assert watson.current['tags'] == ['A', 'B']
+ mocker.patch("builtins.open", mocker.mock_open(read_data=content))
+ assert watson.current["project"] == "foo"
+ assert watson.current["start"] == arrow.get(4000)
+ assert watson.current["tags"] == ["A", "B"]
def test_current_with_empty_file(mocker, watson):
- mocker.patch('builtins.open', mocker.mock_open(read_data=""))
- mocker.patch('os.path.getsize', return_value=0)
+ mocker.patch("builtins.open", mocker.mock_open(read_data=""))
+ mocker.patch("os.path.getsize", return_value=0)
assert watson.current == {}
def test_current_with_nonexistent_file(mocker, watson):
- mocker.patch('builtins.open', side_effect=IOError)
+ mocker.patch("builtins.open", side_effect=IOError)
assert watson.current == {}
def test_current_watson_non_valid_json(mocker, watson):
content = "{'foo': bar}"
- mocker.patch('builtins.open', mocker.mock_open(read_data=content))
- mocker.patch('os.path.getsize', return_value=len(content))
+ mocker.patch("builtins.open", mocker.mock_open(read_data=content))
+ mocker.patch("os.path.getsize", return_value=len(content))
with pytest.raises(WatsonError):
watson.current
def test_current_with_given_state(config_dir, mocker):
- content = json.dumps({'project': 'foo', 'start': 4000})
- watson = Watson(current={'project': 'bar', 'start': 4000},
- config_dir=config_dir)
+ content = json.dumps({"project": "foo", "start": 4000})
+ watson = Watson(current={"project": "bar", "start": 4000}, config_dir=config_dir)
- mocker.patch('builtins.open', mocker.mock_open(read_data=content))
- assert watson.current['project'] == 'bar'
+ mocker.patch("builtins.open", mocker.mock_open(read_data=content))
+ assert watson.current["project"] == "bar"
def test_current_with_empty_given_state(config_dir, mocker):
- content = json.dumps({'project': 'foo', 'start': 4000})
+ content = json.dumps({"project": "foo", "start": 4000})
watson = Watson(current=[], config_dir=config_dir)
- mocker.patch('builtins.open', mocker.mock_open(read_data=content))
+ mocker.patch("builtins.open", mocker.mock_open(read_data=content))
assert watson.current == {}
@@ -78,42 +76,43 @@ def test_current_as_running_frame(watson):
Ensures frame can be created without a stop date.
Catches #417: editing task in progress throws an exception
"""
- watson.start('foo', tags=['A', 'B'])
+ watson.start("foo", tags=["A", "B"])
cur = watson.current
- frame = Frame(cur['start'], None, cur['project'], None, cur['tags'])
+ frame = Frame(cur["start"], None, cur["project"], None, cur["tags"])
assert frame.stop is None
- assert frame.project == 'foo'
- assert frame.tags == ['A', 'B']
+ assert frame.project == "foo"
+ assert frame.tags == ["A", "B"]
# last_sync
+
def test_last_sync(mocker, watson):
now = arrow.get(4123)
content = json.dumps(now.int_timestamp)
- mocker.patch('builtins.open', mocker.mock_open(read_data=content))
+ mocker.patch("builtins.open", mocker.mock_open(read_data=content))
assert watson.last_sync == now
def test_last_sync_with_empty_file(mocker, watson):
- mocker.patch('builtins.open', mocker.mock_open(read_data=""))
- mocker.patch('os.path.getsize', return_value=0)
+ mocker.patch("builtins.open", mocker.mock_open(read_data=""))
+ mocker.patch("os.path.getsize", return_value=0)
assert watson.last_sync == arrow.get(0)
def test_last_sync_with_nonexistent_file(mocker, watson):
- mocker.patch('builtins.open', side_effect=IOError)
+ mocker.patch("builtins.open", side_effect=IOError)
assert watson.last_sync == arrow.get(0)
def test_last_sync_watson_non_valid_json(mocker, watson):
content = "{'foo': bar}"
- mocker.patch('builtins.open', mocker.mock_open(read_data=content))
- mocker.patch('os.path.getsize', return_value=len(content))
+ mocker.patch("builtins.open", mocker.mock_open(read_data=content))
+ mocker.patch("os.path.getsize", return_value=len(content))
with pytest.raises(WatsonError):
watson.last_sync
@@ -123,7 +122,7 @@ def test_last_sync_with_given_state(config_dir, mocker):
now = arrow.now()
watson = Watson(last_sync=now, config_dir=config_dir)
- mocker.patch('builtins.open', mocker.mock_open(read_data=content))
+ mocker.patch("builtins.open", mocker.mock_open(read_data=content))
assert watson.last_sync == now
@@ -131,124 +130,128 @@ def test_last_sync_with_empty_given_state(config_dir, mocker):
content = json.dumps(123)
watson = Watson(last_sync=None, config_dir=config_dir)
- mocker.patch('builtins.open', mocker.mock_open(read_data=content))
+ mocker.patch("builtins.open", mocker.mock_open(read_data=content))
assert watson.last_sync == arrow.get(0)
# frames
+
def test_frames(mocker, watson):
- content = json.dumps([[4000, 4010, 'foo', None, ['A', 'B', 'C']]])
+ content = json.dumps([[4000, 4010, "foo", None, ["A", "B", "C"]]])
- mocker.patch('builtins.open', mocker.mock_open(read_data=content))
+ mocker.patch("builtins.open", mocker.mock_open(read_data=content))
assert len(watson.frames) == 1
- assert watson.frames[0].project == 'foo'
+ assert watson.frames[0].project == "foo"
assert watson.frames[0].start == arrow.get(4000)
assert watson.frames[0].stop == arrow.get(4010)
- assert watson.frames[0].tags == ['A', 'B', 'C']
+ assert watson.frames[0].tags == ["A", "B", "C"]
def test_frames_without_tags(mocker, watson):
- content = json.dumps([[4000, 4010, 'foo', None]])
+ content = json.dumps([[4000, 4010, "foo", None]])
- mocker.patch('builtins.open', mocker.mock_open(read_data=content))
+ mocker.patch("builtins.open", mocker.mock_open(read_data=content))
assert len(watson.frames) == 1
- assert watson.frames[0].project == 'foo'
+ assert watson.frames[0].project == "foo"
assert watson.frames[0].start == arrow.get(4000)
assert watson.frames[0].stop == arrow.get(4010)
assert watson.frames[0].tags == []
def test_frames_with_empty_file(mocker, watson):
- mocker.patch('builtins.open', mocker.mock_open(read_data=""))
- mocker.patch('os.path.getsize', return_value=0)
+ mocker.patch("builtins.open", mocker.mock_open(read_data=""))
+ mocker.patch("os.path.getsize", return_value=0)
assert len(watson.frames) == 0
def test_frames_with_nonexistent_file(mocker, watson):
- mocker.patch('builtins.open', side_effect=IOError)
+ mocker.patch("builtins.open", side_effect=IOError)
assert len(watson.frames) == 0
def test_frames_watson_non_valid_json(mocker, watson):
content = "{'foo': bar}"
- mocker.patch('builtins.open', mocker.mock_open(read_data=content))
- mocker.patch('os.path.getsize', return_value=len(content))
+ mocker.patch("builtins.open", mocker.mock_open(read_data=content))
+ mocker.patch("os.path.getsize", return_value=len(content))
with pytest.raises(WatsonError):
watson.frames
def test_given_frames(config_dir, mocker):
- content = json.dumps([[4000, 4010, 'foo', None, ['A']]])
- watson = Watson(frames=[[4000, 4010, 'bar', None, ['A', 'B']]],
- config_dir=config_dir)
+ content = json.dumps([[4000, 4010, "foo", None, ["A"]]])
+ watson = Watson(
+ frames=[[4000, 4010, "bar", None, ["A", "B"]]], config_dir=config_dir
+ )
- mocker.patch('builtins.open', mocker.mock_open(read_data=content))
+ mocker.patch("builtins.open", mocker.mock_open(read_data=content))
assert len(watson.frames) == 1
- assert watson.frames[0].project == 'bar'
- assert watson.frames[0].tags == ['A', 'B']
+ assert watson.frames[0].project == "bar"
+ assert watson.frames[0].tags == ["A", "B"]
def test_frames_with_empty_given_state(config_dir, mocker):
- content = json.dumps([[0, 10, 'foo', None, ['A']]])
+ content = json.dumps([[0, 10, "foo", None, ["A"]]])
watson = Watson(frames=[], config_dir=config_dir)
- mocker.patch('builtins.open', mocker.mock_open(read_data=content))
+ mocker.patch("builtins.open", mocker.mock_open(read_data=content))
assert len(watson.frames) == 0
# config
+
def test_empty_config_dir():
watson = Watson()
- assert watson._dir == get_app_dir('watson')
+ assert watson._dir == get_app_dir("watson")
def test_wrong_config(mocker, watson):
content = """
toto
"""
- mocker.patch.object(ConfigParser, 'read', mock_read(content))
+ mocker.patch.object(ConfigParser, "read", mock_read(content))
with pytest.raises(ConfigurationError):
watson.config
def test_empty_config(mocker, watson):
- mocker.patch.object(ConfigParser, 'read', mock_read(''))
+ mocker.patch.object(ConfigParser, "read", mock_read(""))
assert len(watson.config.sections()) == 0
# start
+
def test_start_new_project(watson):
- watson.start('foo', ['A', 'B'])
+ watson.start("foo", ["A", "B"])
assert watson.current != {}
assert watson.is_started is True
- assert watson.current.get('project') == 'foo'
- assert isinstance(watson.current.get('start'), arrow.Arrow)
- assert watson.current.get('tags') == ['A', 'B']
+ assert watson.current.get("project") == "foo"
+ assert isinstance(watson.current.get("start"), arrow.Arrow)
+ assert watson.current.get("tags") == ["A", "B"]
def test_start_new_project_without_tags(watson):
- watson.start('foo')
+ watson.start("foo")
assert watson.current != {}
assert watson.is_started is True
- assert watson.current.get('project') == 'foo'
- assert isinstance(watson.current.get('start'), arrow.Arrow)
- assert watson.current.get('tags') == []
+ assert watson.current.get("project") == "foo"
+ assert isinstance(watson.current.get("start"), arrow.Arrow)
+ assert watson.current.get("tags") == []
def test_start_two_projects(watson):
- watson.start('foo')
+ watson.start("foo")
with pytest.raises(WatsonError):
- watson.start('bar')
+ watson.start("bar")
assert watson.current != {}
- assert watson.current['project'] == 'foo'
+ assert watson.current["project"] == "foo"
assert watson.is_started is True
@@ -258,9 +261,9 @@ def test_start_default_tags(mocker, watson):
my project = A B
"""
- mocker.patch.object(ConfigParser, 'read', mock_read(content))
- watson.start('my project')
- assert watson.current['tags'] == ['A', 'B']
+ mocker.patch.object(ConfigParser, "read", mock_read(content))
+ watson.start("my project")
+ assert watson.current["tags"] == ["A", "B"]
def test_start_default_tags_with_supplementary_input_tags(mocker, watson):
@@ -269,63 +272,64 @@ def test_start_default_tags_with_supplementary_input_tags(mocker, watson):
my project = A B
"""
- mocker.patch.object(ConfigParser, 'read', mock_read(content))
- watson.start('my project', tags=['C', 'D'])
- assert watson.current['tags'] == ['C', 'D', 'A', 'B']
+ mocker.patch.object(ConfigParser, "read", mock_read(content))
+ watson.start("my project", tags=["C", "D"])
+ assert watson.current["tags"] == ["C", "D", "A", "B"]
def test_start_nogap(watson):
- watson.start('foo')
+ watson.start("foo")
watson.stop()
- watson.start('bar', gap=False)
+ watson.start("bar", gap=False)
- assert watson.frames[-1].stop == watson.current['start']
+ assert watson.frames[-1].stop == watson.current["start"]
def test_start_project_at(watson):
now = arrow.now()
- watson.start('foo', start_at=now)
+ watson.start("foo", start_at=now)
watson.stop()
# Task can't start before the previous task ends
with pytest.raises(WatsonError):
- time_str = '1970-01-01T00:00'
+ time_str = "1970-01-01T00:00"
time_obj = arrow.get(time_str)
- watson.start('foo', start_at=time_obj)
+ watson.start("foo", start_at=time_obj)
# Task can't start in the future
with pytest.raises(WatsonError):
- time_str = '2999-12-31T23:59'
+ time_str = "2999-12-31T23:59"
time_obj = arrow.get(time_str)
- watson.start('foo', start_at=time_obj)
+ watson.start("foo", start_at=time_obj)
assert watson.frames[-1].start == now
# stop
+
def test_stop_started_project(watson):
- watson.start('foo', tags=['A', 'B'])
+ watson.start("foo", tags=["A", "B"])
watson.stop()
assert watson.current == {}
assert watson.is_started is False
assert len(watson.frames) == 1
- assert watson.frames[0].project == 'foo'
+ assert watson.frames[0].project == "foo"
assert isinstance(watson.frames[0].start, arrow.Arrow)
assert isinstance(watson.frames[0].stop, arrow.Arrow)
- assert watson.frames[0].tags == ['A', 'B']
+ assert watson.frames[0].tags == ["A", "B"]
def test_stop_started_project_without_tags(watson):
- watson.start('foo')
+ watson.start("foo")
watson.stop()
assert watson.current == {}
assert watson.is_started is False
assert len(watson.frames) == 1
- assert watson.frames[0].project == 'foo'
+ assert watson.frames[0].project == "foo"
assert isinstance(watson.frames[0].start, arrow.Arrow)
assert isinstance(watson.frames[0].stop, arrow.Arrow)
assert watson.frames[0].tags == []
@@ -337,18 +341,18 @@ def test_stop_no_project(watson):
def test_stop_started_project_at(watson):
- watson.start('foo')
+ watson.start("foo")
now = arrow.now()
# Task can't end before it starts
with pytest.raises(WatsonError):
- time_str = '1970-01-01T00:00'
+ time_str = "1970-01-01T00:00"
time_obj = arrow.get(time_str)
watson.stop(stop_at=time_obj)
# Task can't end in the future
with pytest.raises(WatsonError):
- time_str = '2999-12-31T23:59'
+ time_str = "2999-12-31T23:59"
time_obj = arrow.get(time_str)
watson.stop(stop_at=time_obj)
@@ -358,8 +362,9 @@ def test_stop_started_project_at(watson):
# cancel
+
def test_cancel_started_project(watson):
- watson.start('foo')
+ watson.start("foo")
watson.cancel()
assert watson.current == {}
@@ -373,53 +378,54 @@ def test_cancel_no_project(watson):
# save
+
def test_save_without_changes(mocker, watson, json_mock):
- mocker.patch('builtins.open', mocker.mock_open())
+ mocker.patch("builtins.open", mocker.mock_open())
watson.save()
assert not json_mock.called
def test_save_current(mocker, watson, json_mock):
- watson.start('foo', ['A', 'B'])
+ watson.start("foo", ["A", "B"])
- mocker.patch('builtins.open', mocker.mock_open())
+ mocker.patch("builtins.open", mocker.mock_open())
watson.save()
assert json_mock.call_count == 1
result = json_mock.call_args[0][0]
- assert result['project'] == 'foo'
- assert isinstance(result['start'], (int, float))
- assert result['tags'] == ['A', 'B']
+ assert result["project"] == "foo"
+ assert isinstance(result["start"], (int, float))
+ assert result["tags"] == ["A", "B"]
def test_save_current_without_tags(mocker, watson, json_mock):
- watson.start('foo')
+ watson.start("foo")
- mocker.patch('builtins.open', mocker.mock_open())
+ mocker.patch("builtins.open", mocker.mock_open())
watson.save()
assert json_mock.call_count == 1
result = json_mock.call_args[0][0]
- assert result['project'] == 'foo'
- assert isinstance(result['start'], (int, float))
- assert result['tags'] == []
+ assert result["project"] == "foo"
+ assert isinstance(result["start"], (int, float))
+ assert result["tags"] == []
dump_args = json_mock.call_args[1]
- assert dump_args['ensure_ascii'] is False
+ assert dump_args["ensure_ascii"] is False
def test_save_empty_current(config_dir, mocker, json_mock):
watson = Watson(current={}, config_dir=config_dir)
- mocker.patch('builtins.open', mocker.mock_open())
+ mocker.patch("builtins.open", mocker.mock_open())
- watson.current = {'project': 'foo', 'start': 4000}
+ watson.current = {"project": "foo", "start": 4000}
watson.save()
assert json_mock.call_count == 1
result = json_mock.call_args[0][0]
- assert result == {'project': 'foo', 'start': 4000, 'tags': []}
+ assert result == {"project": "foo", "start": 4000, "tags": []}
watson.current = {}
watson.save()
@@ -430,60 +436,58 @@ def test_save_empty_current(config_dir, mocker, json_mock):
def test_save_frames_no_change(config_dir, mocker, json_mock):
- watson = Watson(frames=[[4000, 4010, 'foo', None]],
- config_dir=config_dir)
+ watson = Watson(frames=[[4000, 4010, "foo", None]], config_dir=config_dir)
- mocker.patch('builtins.open', mocker.mock_open())
+ mocker.patch("builtins.open", mocker.mock_open())
watson.save()
assert not json_mock.called
def test_save_added_frame(config_dir, mocker, json_mock):
- watson = Watson(frames=[[4000, 4010, 'foo', None]], config_dir=config_dir)
- watson.frames.add('bar', 4010, 4020, ['A'])
+ watson = Watson(frames=[[4000, 4010, "foo", None]], config_dir=config_dir)
+ watson.frames.add("bar", 4010, 4020, ["A"])
- mocker.patch('builtins.open', mocker.mock_open())
+ mocker.patch("builtins.open", mocker.mock_open())
watson.save()
assert json_mock.call_count == 1
result = json_mock.call_args[0][0]
assert len(result) == 2
- assert result[0][2] == 'foo'
+ assert result[0][2] == "foo"
assert result[0][4] == []
- assert result[1][2] == 'bar'
- assert result[1][4] == ['A']
+ assert result[1][2] == "bar"
+ assert result[1][4] == ["A"]
def test_save_changed_frame(config_dir, mocker, json_mock):
- watson = Watson(frames=[[4000, 4010, 'foo', None, ['A']]],
- config_dir=config_dir)
- watson.frames[0] = ('bar', 4000, 4010, ['A', 'B'])
+ watson = Watson(frames=[[4000, 4010, "foo", None, ["A"]]], config_dir=config_dir)
+ watson.frames[0] = ("bar", 4000, 4010, ["A", "B"])
- mocker.patch('builtins.open', mocker.mock_open())
+ mocker.patch("builtins.open", mocker.mock_open())
watson.save()
assert json_mock.call_count == 1
result = json_mock.call_args[0][0]
assert len(result) == 1
- assert result[0][2] == 'bar'
- assert result[0][4] == ['A', 'B']
+ assert result[0][2] == "bar"
+ assert result[0][4] == ["A", "B"]
dump_args = json_mock.call_args[1]
- assert dump_args['ensure_ascii'] is False
+ assert dump_args["ensure_ascii"] is False
def test_save_config_no_changes(mocker, watson):
- mocker.patch('builtins.open', mocker.mock_open())
- write_mock = mocker.patch.object(ConfigParser, 'write')
+ mocker.patch("builtins.open", mocker.mock_open())
+ write_mock = mocker.patch.object(ConfigParser, "write")
watson.save()
assert not write_mock.called
def test_save_config(mocker, watson):
- mocker.patch('builtins.open', mocker.mock_open())
- write_mock = mocker.patch.object(ConfigParser, 'write')
+ mocker.patch("builtins.open", mocker.mock_open())
+ write_mock = mocker.patch.object(ConfigParser, "write")
watson.config = ConfigParser()
watson.save()
@@ -494,7 +498,7 @@ def test_save_last_sync(mocker, watson, json_mock):
now = arrow.now()
watson.last_sync = now
- mocker.patch('builtins.open', mocker.mock_open())
+ mocker.patch("builtins.open", mocker.mock_open())
watson.save()
assert json_mock.call_count == 1
@@ -505,7 +509,7 @@ def test_save_empty_last_sync(config_dir, mocker, json_mock):
watson = Watson(last_sync=arrow.now(), config_dir=config_dir)
watson.last_sync = None
- mocker.patch('builtins.open', mocker.mock_open())
+ mocker.patch("builtins.open", mocker.mock_open())
watson.save()
assert json_mock.call_count == 1
@@ -513,11 +517,11 @@ def test_save_empty_last_sync(config_dir, mocker, json_mock):
def test_watson_save_calls_safe_save(mocker, config_dir, watson):
- frames_file = os.path.join(config_dir, 'frames')
- watson.start('foo', tags=['A', 'B'])
+ frames_file = os.path.join(config_dir, "frames")
+ watson.start("foo", tags=["A", "B"])
watson.stop()
- save_mock = mocker.patch('watson.watson.safe_save')
+ save_mock = mocker.patch("watson.watson.safe_save")
watson.save()
assert watson._frames.changed
@@ -528,6 +532,7 @@ def test_watson_save_calls_safe_save(mocker, config_dir, watson):
# push
+
def test_push_with_no_config(watson):
config = ConfigParser()
watson.config = config
@@ -538,8 +543,8 @@ def test_push_with_no_config(watson):
def test_push_with_no_url(watson):
config = ConfigParser()
- config.add_section('backend')
- config.set('backend', 'token', 'bar')
+ config.add_section("backend")
+ config.set("backend", "token", "bar")
watson.config = config
with pytest.raises(WatsonError):
@@ -548,8 +553,8 @@ def test_push_with_no_url(watson):
def test_push_with_no_token(watson):
config = ConfigParser()
- config.add_section('backend')
- config.set('backend', 'url', 'http://foo.com')
+ config.add_section("backend")
+ config.set("backend", "url", "http://foo.com")
watson.config = config
with pytest.raises(WatsonError):
@@ -558,36 +563,40 @@ def test_push_with_no_token(watson):
def test_push(mocker, watson):
config = ConfigParser()
- config.add_section('backend')
- config.set('backend', 'url', 'http://foo.com')
- config.set('backend', 'token', 'bar')
+ config.add_section("backend")
+ config.set("backend", "url", "http://foo.com")
+ config.set("backend", "token", "bar")
- watson.frames.add('foo', 4001, 4002)
- watson.frames.add('foo', 4003, 4004)
+ watson.frames.add("foo", 4001, 4002)
+ watson.frames.add("foo", 4003, 4004)
watson.last_sync = arrow.now()
- watson.frames.add('bar', 4001, 4002, ['A', 'B'])
- watson.frames.add('lol', 4001, 4002)
+ watson.frames.add("bar", 4001, 4002, ["A", "B"])
+ watson.frames.add("lol", 4001, 4002)
last_pull = arrow.now()
- watson.frames.add('foo', 4001, 4002)
- watson.frames.add('bar', 4003, 4004)
+ watson.frames.add("foo", 4001, 4002)
+ watson.frames.add("bar", 4003, 4004)
- mocker.patch.object(watson, '_get_remote_projects', return_value=[
- {'name': 'foo', 'id': '08288b71-4500-40dd-96b1-a995937a15fd'},
- {'name': 'bar', 'id': 'f0534272-65fa-4832-a49e-0eedf68b3a84'},
- {'name': 'lol', 'id': '7fdaf65e-66bd-4c01-b09e-74bdc8cbe552'},
- ])
+ mocker.patch.object(
+ watson,
+ "_get_remote_projects",
+ return_value=[
+ {"name": "foo", "id": "08288b71-4500-40dd-96b1-a995937a15fd"},
+ {"name": "bar", "id": "f0534272-65fa-4832-a49e-0eedf68b3a84"},
+ {"name": "lol", "id": "7fdaf65e-66bd-4c01-b09e-74bdc8cbe552"},
+ ],
+ )
class Response:
def __init__(self):
self.status_code = 201
- mock_put = mocker.patch('requests.post', return_value=Response())
+ mock_put = mocker.patch("requests.post", return_value=Response())
mocker.patch.object(
- Watson, 'config', new_callable=mocker.PropertyMock, return_value=config
+ Watson, "config", new_callable=mocker.PropertyMock, return_value=config
)
watson.push(last_pull)
@@ -595,23 +604,24 @@ def __init__(self):
mocker.ANY,
mocker.ANY,
headers={
- 'content-type': 'application/json',
- 'Authorization': "Token " + config.get('backend', 'token')
- }
+ "content-type": "application/json",
+ "Authorization": "Token " + config.get("backend", "token"),
+ },
)
frames_sent = json.loads(mock_put.call_args[0][1])
assert len(frames_sent) == 2
- assert frames_sent[0].get('project') == 'bar'
- assert frames_sent[0].get('tags') == ['A', 'B']
+ assert frames_sent[0].get("project") == "bar"
+ assert frames_sent[0].get("tags") == ["A", "B"]
- assert frames_sent[1].get('project') == 'lol'
- assert frames_sent[1].get('tags') == []
+ assert frames_sent[1].get("project") == "lol"
+ assert frames_sent[1].get("tags") == []
# pull
+
def test_pull_with_no_config(watson):
config = ConfigParser()
watson.config = config
@@ -622,8 +632,8 @@ def test_pull_with_no_config(watson):
def test_pull_with_no_url(watson):
config = ConfigParser()
- config.add_section('backend')
- config.set('backend', 'token', 'bar')
+ config.add_section("backend")
+ config.set("backend", "token", "bar")
watson.config = config
with pytest.raises(ConfigurationError):
@@ -632,8 +642,8 @@ def test_pull_with_no_url(watson):
def test_pull_with_no_token(watson):
config = ConfigParser()
- config.add_section('backend')
- config.set('backend', 'url', 'http://foo.com')
+ config.add_section("backend")
+ config.set("backend", "url", "http://foo.com")
watson.config = config
with pytest.raises(ConfigurationError):
@@ -642,20 +652,24 @@ def test_pull_with_no_token(watson):
def test_pull(mocker, watson):
config = ConfigParser()
- config.add_section('backend')
- config.set('backend', 'url', 'http://foo.com')
- config.set('backend', 'token', 'bar')
+ config.add_section("backend")
+ config.set("backend", "url", "http://foo.com")
+ config.set("backend", "token", "bar")
watson.last_sync = arrow.now()
watson.frames.add(
- 'foo', 4001, 4002, ['A', 'B'], id='1c006c6e6cc14c80ab22b51c857c0b06'
+ "foo", 4001, 4002, ["A", "B"], id="1c006c6e6cc14c80ab22b51c857c0b06"
)
- mocker.patch.object(watson, '_get_remote_projects', return_value=[
- {'name': 'foo', 'id': '08288b71-4500-40dd-96b1-a995937a15fd'},
- {'name': 'bar', 'id': 'f0534272-65fa-4832-a49e-0eedf68b3a84'},
- ])
+ mocker.patch.object(
+ watson,
+ "_get_remote_projects",
+ return_value=[
+ {"name": "foo", "id": "08288b71-4500-40dd-96b1-a995937a15fd"},
+ {"name": "bar", "id": "f0534272-65fa-4832-a49e-0eedf68b3a84"},
+ ],
+ )
class Response:
def __init__(self):
@@ -664,46 +678,46 @@ def __init__(self):
def json(self):
return [
{
- 'id': '1c006c6e-6cc1-4c80-ab22-b51c857c0b06',
- 'project': 'foo',
- 'begin_at': 4003,
- 'end_at': 4004,
- 'tags': ['A']
+ "id": "1c006c6e-6cc1-4c80-ab22-b51c857c0b06",
+ "project": "foo",
+ "begin_at": 4003,
+ "end_at": 4004,
+ "tags": ["A"],
},
{
- 'id': 'c44aa815-4d77-4a58-bddd-1afa95562141',
- 'project': 'bar',
- 'begin_at': 4004,
- 'end_at': 4005,
- 'tags': []
- }
+ "id": "c44aa815-4d77-4a58-bddd-1afa95562141",
+ "project": "bar",
+ "begin_at": 4004,
+ "end_at": 4005,
+ "tags": [],
+ },
]
- mocker.patch('requests.get', return_value=Response())
+ mocker.patch("requests.get", return_value=Response())
mocker.patch.object(
- Watson, 'config', new_callable=mocker.PropertyMock, return_value=config
+ Watson, "config", new_callable=mocker.PropertyMock, return_value=config
)
watson.pull()
requests.get.assert_called_once_with(
mocker.ANY,
- params={'last_sync': watson.last_sync},
+ params={"last_sync": watson.last_sync},
headers={
- 'content-type': 'application/json',
- 'Authorization': "Token " + config.get('backend', 'token')
- }
+ "content-type": "application/json",
+ "Authorization": "Token " + config.get("backend", "token"),
+ },
)
assert len(watson.frames) == 2
- assert watson.frames[0].id == '1c006c6e6cc14c80ab22b51c857c0b06'
- assert watson.frames[0].project == 'foo'
+ assert watson.frames[0].id == "1c006c6e6cc14c80ab22b51c857c0b06"
+ assert watson.frames[0].project == "foo"
assert watson.frames[0].start.int_timestamp == 4003
assert watson.frames[0].stop.int_timestamp == 4004
- assert watson.frames[0].tags == ['A']
+ assert watson.frames[0].tags == ["A"]
- assert watson.frames[1].id == 'c44aa8154d774a58bddd1afa95562141'
- assert watson.frames[1].project == 'bar'
+ assert watson.frames[1].id == "c44aa8154d774a58bddd1afa95562141"
+ assert watson.frames[1].project == "bar"
assert watson.frames[1].start.int_timestamp == 4004
assert watson.frames[1].stop.int_timestamp == 4005
assert watson.frames[1].tags == []
@@ -711,11 +725,12 @@ def json(self):
# projects
+
def test_projects(watson):
- for name in ('foo', 'bar', 'bar', 'bar', 'foo', 'lol'):
+ for name in ("foo", "bar", "bar", "bar", "foo", "lol"):
watson.frames.add(name, 4000, 4000)
- assert watson.projects == ['bar', 'foo', 'lol']
+ assert watson.projects == ["bar", "foo", "lol"]
def test_projects_no_frames(watson):
@@ -724,19 +739,20 @@ def test_projects_no_frames(watson):
# tags
+
def test_tags(watson):
samples = (
- ('foo', ('A', 'D')),
- ('bar', ('A', 'C')),
- ('foo', ('B', 'C')),
- ('lol', ()),
- ('bar', ('C'))
+ ("foo", ("A", "D")),
+ ("bar", ("A", "C")),
+ ("foo", ("B", "C")),
+ ("lol", ()),
+ ("bar", ("C")),
)
for name, tags in samples:
watson.frames.add(name, 4000, 4000, tags)
- assert watson.tags == ['A', 'B', 'C', 'D']
+ assert watson.tags == ["A", "B", "C", "D"]
def test_tags_no_frames(watson):
@@ -745,73 +761,71 @@ def test_tags_no_frames(watson):
# merge
+
@pytest.mark.datafiles(
- TEST_FIXTURE_DIR / 'frames-with-conflict',
- )
+ TEST_FIXTURE_DIR / "frames-with-conflict",
+)
def test_merge_report(watson, datafiles):
# Get report
- watson.frames.add('foo', 4000, 4015, id='1', updated_at=4015)
- watson.frames.add('bar', 4020, 4045, id='2', updated_at=4045)
+ watson.frames.add("foo", 4000, 4015, id="1", updated_at=4015)
+ watson.frames.add("bar", 4020, 4045, id="2", updated_at=4045)
- conflicting, merging = watson.merge_report(
- str(datafiles) + '/frames-with-conflict')
+ conflicting, merging = watson.merge_report(str(datafiles) + "/frames-with-conflict")
assert len(conflicting) == 1
assert len(merging) == 1
- assert conflicting[0].id == '2'
- assert merging[0].id == '3'
+ assert conflicting[0].id == "2"
+ assert merging[0].id == "3"
def test_report(watson):
- watson.start('foo', tags=['A', 'B'])
+ watson.start("foo", tags=["A", "B"])
watson.stop()
report = watson.report(arrow.now(), arrow.now())
- assert 'time' in report
- assert 'timespan' in report
- assert 'from' in report['timespan']
- assert 'to' in report['timespan']
- assert len(report['projects']) == 1
- assert report['projects'][0]['name'] == 'foo'
- assert len(report['projects'][0]['tags']) == 2
- assert report['projects'][0]['tags'][0]['name'] == 'A'
- assert 'time' in report['projects'][0]['tags'][0]
- assert report['projects'][0]['tags'][1]['name'] == 'B'
- assert 'time' in report['projects'][0]['tags'][1]
-
- watson.start('bar', tags=['C'])
+ assert "time" in report
+ assert "timespan" in report
+ assert "from" in report["timespan"]
+ assert "to" in report["timespan"]
+ assert len(report["projects"]) == 1
+ assert report["projects"][0]["name"] == "foo"
+ assert len(report["projects"][0]["tags"]) == 2
+ assert report["projects"][0]["tags"][0]["name"] == "A"
+ assert "time" in report["projects"][0]["tags"][0]
+ assert report["projects"][0]["tags"][1]["name"] == "B"
+ assert "time" in report["projects"][0]["tags"][1]
+
+ watson.start("bar", tags=["C"])
watson.stop()
report = watson.report(arrow.now(), arrow.now())
- assert len(report['projects']) == 2
- assert report['projects'][0]['name'] == 'bar'
- assert report['projects'][1]['name'] == 'foo'
- assert len(report['projects'][0]['tags']) == 1
- assert report['projects'][0]['tags'][0]['name'] == 'C'
-
- report = watson.report(
- arrow.now(), arrow.now(), projects=['foo'], tags=['B']
- )
- assert len(report['projects']) == 1
- assert report['projects'][0]['name'] == 'foo'
- assert len(report['projects'][0]['tags']) == 1
- assert report['projects'][0]['tags'][0]['name'] == 'B'
-
- watson.start('baz', tags=['D'])
+ assert len(report["projects"]) == 2
+ assert report["projects"][0]["name"] == "bar"
+ assert report["projects"][1]["name"] == "foo"
+ assert len(report["projects"][0]["tags"]) == 1
+ assert report["projects"][0]["tags"][0]["name"] == "C"
+
+ report = watson.report(arrow.now(), arrow.now(), projects=["foo"], tags=["B"])
+ assert len(report["projects"]) == 1
+ assert report["projects"][0]["name"] == "foo"
+ assert len(report["projects"][0]["tags"]) == 1
+ assert report["projects"][0]["tags"][0]["name"] == "B"
+
+ watson.start("baz", tags=["D"])
watson.stop()
report = watson.report(arrow.now(), arrow.now(), projects=["foo"])
- assert len(report['projects']) == 1
+ assert len(report["projects"]) == 1
report = watson.report(arrow.now(), arrow.now(), ignore_projects=["bar"])
- assert len(report['projects']) == 2
+ assert len(report["projects"]) == 2
report = watson.report(arrow.now(), arrow.now(), tags=["A"])
- assert len(report['projects']) == 1
+ assert len(report["projects"]) == 1
report = watson.report(arrow.now(), arrow.now(), ignore_tags=["D"])
- assert len(report['projects']) == 2
+ assert len(report["projects"]) == 2
with pytest.raises(WatsonError):
watson.report(
@@ -823,42 +837,39 @@ def test_report(watson):
def test_report_current(mocker, config_dir):
- mocker.patch('arrow.utcnow', return_value=arrow.get(5000))
+ mocker.patch("arrow.utcnow", return_value=arrow.get(5000))
- watson = Watson(
- current={'project': 'foo', 'start': 4000},
- config_dir=config_dir
- )
+ watson = Watson(current={"project": "foo", "start": 4000}, config_dir=config_dir)
for _ in range(2):
report = watson.report(
- arrow.utcnow(), arrow.utcnow(), current=True, projects=['foo']
+ arrow.utcnow(), arrow.utcnow(), current=True, projects=["foo"]
)
- assert len(report['projects']) == 1
- assert report['projects'][0]['name'] == 'foo'
- assert report['projects'][0]['time'] == pytest.approx(1000)
+ assert len(report["projects"]) == 1
+ assert report["projects"][0]["name"] == "foo"
+ assert report["projects"][0]["time"] == pytest.approx(1000)
report = watson.report(
- arrow.utcnow(), arrow.utcnow(), current=False, projects=['foo']
+ arrow.utcnow(), arrow.utcnow(), current=False, projects=["foo"]
)
- assert len(report['projects']) == 0
+ assert len(report["projects"]) == 0
- report = watson.report(
- arrow.utcnow(), arrow.utcnow(), projects=['foo']
- )
- assert len(report['projects']) == 0
+ report = watson.report(arrow.utcnow(), arrow.utcnow(), projects=["foo"])
+ assert len(report["projects"]) == 0
@pytest.mark.parametrize(
- "date_as_unixtime,include_partial,sum_", (
+ "date_as_unixtime,include_partial,sum_",
+ (
(3600 * 24, False, 0.0),
(3600 * 48, False, 0.0),
(3600 * 24, True, 7200.0),
(3600 * 48, True, 3600.0),
- )
+ ),
)
-def test_report_include_partial_frames(mocker, watson, date_as_unixtime,
- include_partial, sum_):
+def test_report_include_partial_frames(
+ mocker, watson, date_as_unixtime, include_partial, sum_
+):
"""Test report building with frames that cross report boundaries
1 event is added that has 2 hours in one day and 1 in the next. The
@@ -867,18 +878,24 @@ def test_report_include_partial_frames(mocker, watson, date_as_unixtime,
`include_partial=False`
"""
- content = json.dumps([[
- 3600 * 46,
- 3600 * 49,
- "programming",
- "3e76c820909840f89cabaf106ab7d12a",
- ["cli"],
- 1548797432
- ]])
- mocker.patch('builtins.open', mocker.mock_open(read_data=content))
+ content = json.dumps(
+ [
+ [
+ 3600 * 46,
+ 3600 * 49,
+ "programming",
+ "3e76c820909840f89cabaf106ab7d12a",
+ ["cli"],
+ 1548797432,
+ ]
+ ]
+ )
+ mocker.patch("builtins.open", mocker.mock_open(read_data=content))
date = arrow.get(date_as_unixtime)
report = watson.report(
- from_=date, to=date, include_partial_frames=include_partial,
+ from_=date,
+ to=date,
+ include_partial_frames=include_partial,
)
assert report["time"] == pytest.approx(sum_, abs=1e-3)
@@ -890,31 +907,39 @@ def test_rename_project_with_time(watson):
contains that project.
"""
watson.frames.add(
- 'foo', 4001, 4002, ['some_tag'],
- id='c76d1ad0282c429595cc566d7098c165', updated_at=4005
+ "foo",
+ 4001,
+ 4002,
+ ["some_tag"],
+ id="c76d1ad0282c429595cc566d7098c165",
+ updated_at=4005,
)
watson.frames.add(
- 'bar', 4010, 4015, ['other_tag'],
- id='eed598ff363d42658a095ae6c3ae1088', updated_at=4035
+ "bar",
+ 4010,
+ 4015,
+ ["other_tag"],
+ id="eed598ff363d42658a095ae6c3ae1088",
+ updated_at=4035,
)
watson.rename_project("foo", "baz")
assert len(watson.frames) == 2
- assert watson.frames[0].id == 'c76d1ad0282c429595cc566d7098c165'
- assert watson.frames[0].project == 'baz'
+ assert watson.frames[0].id == "c76d1ad0282c429595cc566d7098c165"
+ assert watson.frames[0].project == "baz"
assert watson.frames[0].start.int_timestamp == 4001
assert watson.frames[0].stop.int_timestamp == 4002
- assert watson.frames[0].tags == ['some_tag']
+ assert watson.frames[0].tags == ["some_tag"]
# assert watson.frames[0].updated_at.int_timestamp == 9000
assert watson.frames[0].updated_at.int_timestamp > 4005
- assert watson.frames[1].id == 'eed598ff363d42658a095ae6c3ae1088'
- assert watson.frames[1].project == 'bar'
+ assert watson.frames[1].id == "eed598ff363d42658a095ae6c3ae1088"
+ assert watson.frames[1].project == "bar"
assert watson.frames[1].start.int_timestamp == 4010
assert watson.frames[1].stop.int_timestamp == 4015
- assert watson.frames[1].tags == ['other_tag']
+ assert watson.frames[1].tags == ["other_tag"]
assert watson.frames[1].updated_at.int_timestamp == 4035
@@ -924,33 +949,42 @@ def test_rename_tag_with_time(watson):
contains that tag.
"""
watson.frames.add(
- 'foo', 4001, 4002, ['some_tag'],
- id='c76d1ad0282c429595cc566d7098c165', updated_at=4005
+ "foo",
+ 4001,
+ 4002,
+ ["some_tag"],
+ id="c76d1ad0282c429595cc566d7098c165",
+ updated_at=4005,
)
watson.frames.add(
- 'bar', 4010, 4015, ['other_tag'],
- id='eed598ff363d42658a095ae6c3ae1088', updated_at=4035
+ "bar",
+ 4010,
+ 4015,
+ ["other_tag"],
+ id="eed598ff363d42658a095ae6c3ae1088",
+ updated_at=4035,
)
watson.rename_tag("other_tag", "baz")
assert len(watson.frames) == 2
- assert watson.frames[0].id == 'c76d1ad0282c429595cc566d7098c165'
- assert watson.frames[0].project == 'foo'
+ assert watson.frames[0].id == "c76d1ad0282c429595cc566d7098c165"
+ assert watson.frames[0].project == "foo"
assert watson.frames[0].start.int_timestamp == 4001
assert watson.frames[0].stop.int_timestamp == 4002
- assert watson.frames[0].tags == ['some_tag']
+ assert watson.frames[0].tags == ["some_tag"]
assert watson.frames[0].updated_at.int_timestamp == 4005
- assert watson.frames[1].id == 'eed598ff363d42658a095ae6c3ae1088'
- assert watson.frames[1].project == 'bar'
+ assert watson.frames[1].id == "eed598ff363d42658a095ae6c3ae1088"
+ assert watson.frames[1].project == "bar"
assert watson.frames[1].start.int_timestamp == 4010
assert watson.frames[1].stop.int_timestamp == 4015
- assert watson.frames[1].tags == ['baz']
+ assert watson.frames[1].tags == ["baz"]
# assert watson.frames[1].updated_at.int_timestamp == 9000
assert watson.frames[1].updated_at.int_timestamp > 4035
+
# add
@@ -958,12 +992,13 @@ def test_add_success(watson):
"""
Adding a new frame outside of live tracking successfully
"""
- watson.add(project="test_project", tags=['fuu', 'bar'],
- from_date=6000, to_date=7000)
+ watson.add(
+ project="test_project", tags=["fuu", "bar"], from_date=6000, to_date=7000
+ )
assert len(watson.frames) == 1
assert watson.frames[0].project == "test_project"
- assert 'fuu' in watson.frames[0].tags
+ assert "fuu" in watson.frames[0].tags
def test_add_failure(watson):
@@ -972,18 +1007,20 @@ def test_add_failure(watson):
to date is before from date
"""
with pytest.raises(WatsonError):
- watson.add(project="test_project", tags=['fuu', 'bar'],
- from_date=7000, to_date=6000)
+ watson.add(
+ project="test_project", tags=["fuu", "bar"], from_date=7000, to_date=6000
+ )
def test_validate_report_options(watson):
assert watson._validate_report_options(["project_foo"], None)
assert watson._validate_report_options(None, ["project_foo"])
- assert not watson._validate_report_options(["project_foo"],
- ["project_foo"])
+ assert not watson._validate_report_options(["project_foo"], ["project_foo"])
assert watson._validate_report_options(["project_foo"], ["project_bar"])
- assert not watson._validate_report_options(["project_foo", "project_bar"],
- ["project_foo"])
- assert not watson._validate_report_options(["project_foo", "project_bar"],
- ["project_foo", "project_bar"])
+ assert not watson._validate_report_options(
+ ["project_foo", "project_bar"], ["project_foo"]
+ )
+ assert not watson._validate_report_options(
+ ["project_foo", "project_bar"], ["project_foo", "project_bar"]
+ )
assert watson._validate_report_options(None, None)
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index f500c5e2..00000000
--- a/tox.ini
+++ /dev/null
@@ -1,17 +0,0 @@
-[tox]
-envlist = flake8,py35,py36,py37,py38,py39
-skip_missing_interpreters = True
-
-[testenv]
-deps =
- pytest
- py
- mock
- pytest-datafiles
- pytest-mock
-commands = py.test -vs tests/
-usedevelop = True
-
-[testenv:flake8]
-deps = flake8
-commands = flake8 --show-source watson/ tests/ scripts/
diff --git a/watson/__init__.py b/watson/__init__.py
index 7b408dd9..fe3ea832 100644
--- a/watson/__init__.py
+++ b/watson/__init__.py
@@ -1,4 +1,4 @@
from .watson import __version__ # noqa
from .watson import Watson, WatsonError
-__all__ = ['Watson', 'WatsonError']
+__all__ = ["Watson", "WatsonError"]
diff --git a/watson/autocompletion.py b/watson/autocompletion.py
index 9152b69f..aec22882 100644
--- a/watson/autocompletion.py
+++ b/watson/autocompletion.py
@@ -10,7 +10,7 @@ def _bypass_click_bug_to_ensure_watson(ctx):
def get_project_or_task_completion(ctx, args, incomplete):
"""Function to autocomplete either organisations or tasks, depending on the
- shape of the current argument."""
+ shape of the current argument."""
assert isinstance(incomplete, str)
@@ -47,9 +47,7 @@ def prepend_plus(tag_suggestions):
_bypass_click_bug_to_ensure_watson(ctx)
- project_is_completed = any(
- tok.startswith("+") for tok in args + [incomplete]
- )
+ project_is_completed = any(tok.startswith("+") for tok in args + [incomplete])
if project_is_completed:
incomplete_tag = get_incomplete_tag(args, incomplete)
fixed_incomplete_tag = fix_broken_tag_parsing(incomplete_tag)
diff --git a/watson/cli.py b/watson/cli.py
index bd3bd527..db87b118 100644
--- a/watson/cli.py
+++ b/watson/cli.py
@@ -32,7 +32,8 @@
frames_to_json,
get_frame_from_argument,
get_start_time_for_period,
- options, safe_save,
+ options,
+ safe_save,
sorted_groupby,
style,
parse_tags,
@@ -42,7 +43,7 @@
class MutuallyExclusiveOption(click.Option):
def __init__(self, *args, **kwargs):
- self.mutually_exclusive = set(kwargs.pop('mutually_exclusive', []))
+ self.mutually_exclusive = set(kwargs.pop("mutually_exclusive", []))
super(MutuallyExclusiveOption, self).__init__(*args, **kwargs)
def handle_parse_result(self, ctx, opts, args):
@@ -51,20 +52,23 @@ def handle_parse_result(self, ctx, opts, args):
self._raise_exclusive_error()
if self.multiple and len(set(opts[self.name])) > 1:
self._raise_exclusive_error()
- return super(MutuallyExclusiveOption, self).handle_parse_result(
- ctx, opts, args
- )
+ return super(MutuallyExclusiveOption, self).handle_parse_result(ctx, opts, args)
def _raise_exclusive_error(self):
# Use self.opts[-1] instead of self.name to handle options with a
# different internal name.
- self.mutually_exclusive.add(self.opts[-1].strip('-'))
+ self.mutually_exclusive.add(self.opts[-1].strip("-"))
raise click.ClickException(
style(
- 'error',
- 'The following options are mutually exclusive: '
- '{options}'.format(options=', '.join(
- ['`--{}`'.format(_) for _ in self.mutually_exclusive]))))
+ "error",
+ "The following options are mutually exclusive: "
+ "{options}".format(
+ options=", ".join(
+ ["`--{}`".format(_) for _ in self.mutually_exclusive]
+ )
+ ),
+ )
+ )
def local_tz_info() -> datetime.tzinfo:
@@ -83,15 +87,16 @@ def local_tz_info() -> datetime.tzinfo:
class DateTimeParamType(click.ParamType):
- name = 'datetime'
+ name = "datetime"
def convert(self, value, param, ctx) -> arrow:
if value:
date = self._parse_multiformat(value)
if date is None:
raise click.UsageError(
- "Could not match value '{}' to any supported date format"
- .format(value)
+ "Could not match value '{}' to any supported date format".format(
+ value
+ )
)
# When we parse a date, we want to parse it in the timezone
# expected by the user, so that midnight is midnight in the local
@@ -101,24 +106,20 @@ def convert(self, value, param, ctx) -> arrow:
# Add an offset to match the week beginning specified in the
# configuration
if param.name == "week":
- week_start = ctx.obj.config.get(
- "options", "week_start", "monday")
- date = apply_weekday_offset(
- start_time=date, week_start=week_start)
+ week_start = ctx.obj.config.get("options", "week_start", "monday")
+ date = apply_weekday_offset(start_time=date, week_start=week_start)
return date
def _parse_multiformat(self, value) -> arrow:
date = None
- for fmt in (None, 'HH:mm:ss', 'HH:mm'):
+ for fmt in (None, "HH:mm:ss", "HH:mm"):
try:
if fmt is None:
date = arrow.get(value)
else:
date = arrow.get(value, fmt)
date = arrow.now().replace(
- hour=date.hour,
- minute=date.minute,
- second=date.second
+ hour=date.hour, minute=date.minute, second=date.second
)
break
except (ValueError, TypeError):
@@ -135,14 +136,14 @@ def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except _watson.WatsonError as e:
- raise click.ClickException(style('error', str(e)))
+ raise click.ClickException(style("error", str(e)))
+
return wrapper
@click.group(cls=DYMGroup)
-@click.version_option(version=_watson.__version__, prog_name='Watson')
-@click.option('--color/--no-color', 'color', default=None,
- help="(Don't) color output.")
+@click.version_option(version=_watson.__version__, prog_name="Watson")
+@click.option("--color/--no-color", "color", default=None, help="(Don't) color output.")
@click.pass_context
def cli(ctx, color):
"""
@@ -162,7 +163,7 @@ def cli(ctx, color):
@cli.command()
-@click.argument('command', required=False)
+@click.argument("command", required=False)
@click.pass_context
def help(ctx, command):
"""
@@ -184,36 +185,65 @@ def _start(watson, project, tags, restart=False, start_at=None, gap=True):
"""
Start project with given list of tags and save status.
"""
- current = watson.start(project, tags, restart=restart, start_at=start_at,
- gap=gap,)
- click.echo("Starting project {}{} at {}".format(
- style('project', project),
- (" " if current['tags'] else "") + style('tags', current['tags']),
- style('time', "{:HH:mm}".format(current['start']))
- ))
+ current = watson.start(
+ project,
+ tags,
+ restart=restart,
+ start_at=start_at,
+ gap=gap,
+ )
+ click.echo(
+ "Starting project {}{} at {}".format(
+ style("project", project),
+ (" " if current["tags"] else "") + style("tags", current["tags"]),
+ style("time", "{:HH:mm}".format(current["start"])),
+ )
+ )
watson.save()
@cli.command()
-@click.option('--at', 'at_', type=DateTime, default=None,
- cls=MutuallyExclusiveOption, mutually_exclusive=['gap_'],
- help=('Start frame at this time. Must be in '
- '(YYYY-MM-DDT)?HH:MM(:SS)? format.'))
-@click.option('-g/-G', '--gap/--no-gap', 'gap_', is_flag=True, default=True,
- cls=MutuallyExclusiveOption, mutually_exclusive=['at_'],
- help=("(Don't) leave gap between end time of previous project "
- "and start time of the current."))
-@click.argument('args', nargs=-1,
- shell_complete=get_project_or_task_completion)
-@click.option('-c', '--confirm-new-project', is_flag=True, default=False,
- help="Confirm addition of new project.")
-@click.option('-b', '--confirm-new-tag', is_flag=True, default=False,
- help="Confirm creation of new tag.")
+@click.option(
+ "--at",
+ "at_",
+ type=DateTime,
+ default=None,
+ cls=MutuallyExclusiveOption,
+ mutually_exclusive=["gap_"],
+ help=("Start frame at this time. Must be in " "(YYYY-MM-DDT)?HH:MM(:SS)? format."),
+)
+@click.option(
+ "-g/-G",
+ "--gap/--no-gap",
+ "gap_",
+ is_flag=True,
+ default=True,
+ cls=MutuallyExclusiveOption,
+ mutually_exclusive=["at_"],
+ help=(
+ "(Don't) leave gap between end time of previous project "
+ "and start time of the current."
+ ),
+)
+@click.argument("args", nargs=-1, shell_complete=get_project_or_task_completion)
+@click.option(
+ "-c",
+ "--confirm-new-project",
+ is_flag=True,
+ default=False,
+ help="Confirm addition of new project.",
+)
+@click.option(
+ "-b",
+ "--confirm-new-tag",
+ is_flag=True,
+ default=False,
+ help="Confirm creation of new tag.",
+)
@click.pass_obj
@click.pass_context
@catch_watson_error
-def start(ctx, watson, confirm_new_project, confirm_new_tag, args, at_,
- gap_=True):
+def start(ctx, watson, confirm_new_project, confirm_new_tag, args, at_, gap_=True):
"""
Start monitoring time for the given project.
You can add tags indicating more specifically what you are working on with
@@ -242,46 +272,50 @@ def start(ctx, watson, confirm_new_project, confirm_new_tag, args, at_,
$ watson start apollo11 +module +brakes --no-gap
Starting project apollo11 [module, brakes] at 16:34
"""
- project = ' '.join(
- itertools.takewhile(lambda s: not s.startswith('+'), args)
- )
+ project = " ".join(itertools.takewhile(lambda s: not s.startswith("+"), args))
if not project:
raise click.ClickException("No project given.")
# Confirm creation of new project if that option is set
- if (watson.config.getboolean('options', 'confirm_new_project') or
- confirm_new_project):
+ if (
+ watson.config.getboolean("options", "confirm_new_project")
+ or confirm_new_project
+ ):
confirm_project(project, watson.projects)
# Parse all the tags
tags = parse_tags(args)
# Confirm creation of new tag(s) if that option is set
- if (watson.config.getboolean('options', 'confirm_new_tag') or
- confirm_new_tag):
+ if watson.config.getboolean("options", "confirm_new_tag") or confirm_new_tag:
confirm_tags(tags, watson.tags)
if project and watson.is_started and not gap_:
current = watson.current
- errmsg = ("Project '{}' is already started and '--no-gap' is passed. "
- "Please stop manually.")
- raise click.ClickException(
- style(
- 'error', errmsg.format(current['project'])
- )
+ errmsg = (
+ "Project '{}' is already started and '--no-gap' is passed. "
+ "Please stop manually."
)
+ raise click.ClickException(style("error", errmsg.format(current["project"])))
- if (project and watson.is_started and
- watson.config.getboolean('options', 'stop_on_start')):
+ if (
+ project
+ and watson.is_started
+ and watson.config.getboolean("options", "stop_on_start")
+ ):
ctx.invoke(stop)
_start(watson, project, tags, start_at=at_, gap=gap_)
-@cli.command(context_settings={'ignore_unknown_options': True})
-@click.option('--at', 'at_', type=DateTime, default=None,
- help=('Stop frame at this time. Must be in '
- '(YYYY-MM-DDT)?HH:MM(:SS)? format.'))
+@cli.command(context_settings={"ignore_unknown_options": True})
+@click.option(
+ "--at",
+ "at_",
+ type=DateTime,
+ default=None,
+ help=("Stop frame at this time. Must be in " "(YYYY-MM-DDT)?HH:MM(:SS)? format."),
+)
@click.pass_obj
@catch_watson_error
def stop(watson, at_):
@@ -300,28 +334,49 @@ def stop(watson, at_):
"""
frame = watson.stop(stop_at=at_)
output_str = "Stopping project {}{}, started {} and stopped {}. (id: {})"
- click.echo(output_str.format(
- style('project', frame.project),
- (" " if frame.tags else "") + style('tags', frame.tags),
- style('time', frame.start.humanize()),
- style('time', frame.stop.humanize()),
- style('short_id', frame.id),
- ))
+ click.echo(
+ output_str.format(
+ style("project", frame.project),
+ (" " if frame.tags else "") + style("tags", frame.tags),
+ style("time", frame.start.humanize()),
+ style("time", frame.stop.humanize()),
+ style("short_id", frame.id),
+ )
+ )
watson.save()
-@cli.command(context_settings={'ignore_unknown_options': True})
-@click.option('--at', 'at_', type=DateTime, default=None,
- cls=MutuallyExclusiveOption, mutually_exclusive=['gap_'],
- help=('Start frame at this time. Must be in '
- '(YYYY-MM-DDT)?HH:MM(:SS)? format.'))
-@click.option('-g/-G', '--gap/--no-gap', 'gap_', is_flag=True, default=True,
- cls=MutuallyExclusiveOption, mutually_exclusive=['at_'],
- help=("(Don't) leave gap between end time of previous project "
- "and start time of the current."))
-@click.option('-s/-S', '--stop/--no-stop', 'stop_', default=None,
- help="(Don't) Stop an already running project.")
-@click.argument('id', default='-1', shell_complete=get_frames)
+@cli.command(context_settings={"ignore_unknown_options": True})
+@click.option(
+ "--at",
+ "at_",
+ type=DateTime,
+ default=None,
+ cls=MutuallyExclusiveOption,
+ mutually_exclusive=["gap_"],
+ help=("Start frame at this time. Must be in " "(YYYY-MM-DDT)?HH:MM(:SS)? format."),
+)
+@click.option(
+ "-g/-G",
+ "--gap/--no-gap",
+ "gap_",
+ is_flag=True,
+ default=True,
+ cls=MutuallyExclusiveOption,
+ mutually_exclusive=["at_"],
+ help=(
+ "(Don't) leave gap between end time of previous project "
+ "and start time of the current."
+ ),
+)
+@click.option(
+ "-s/-S",
+ "--stop/--no-stop",
+ "stop_",
+ default=None,
+ help="(Don't) Stop an already running project.",
+)
+@click.argument("id", default="-1", shell_complete=get_frames)
@click.pass_obj
@click.pass_context
@catch_watson_error
@@ -359,35 +414,39 @@ def restart(ctx, watson, id, stop_, at_, gap_=True):
"""
if not watson.frames and not watson.is_started:
raise click.ClickException(
- style('error', "No frames recorded yet. It's time to create your "
- "first one!"))
+ style(
+ "error",
+ "No frames recorded yet. It's time to create your " "first one!",
+ )
+ )
if watson.is_started and not gap_:
current = watson.current
- errmsg = ("Project '{}' is already started and '--no-gap' is passed. "
- "Please stop manually.")
- raise click.ClickException(
- style(
- 'error', errmsg.format(current['project'])
- )
+ errmsg = (
+ "Project '{}' is already started and '--no-gap' is passed. "
+ "Please stop manually."
)
+ raise click.ClickException(style("error", errmsg.format(current["project"])))
if watson.is_started:
- if stop_ or (stop_ is None and
- watson.config.getboolean('options', 'stop_on_restart')):
+ if stop_ or (
+ stop_ is None and watson.config.getboolean("options", "stop_on_restart")
+ ):
ctx.invoke(stop)
else:
# Raise error here, instead of in watson.start(), otherwise
# will give misleading error if running frame is the first one
- raise click.ClickException("{} {} {}".format(
- style('error', "Project already started:"),
- style('project', watson.current['project']),
- style('tags', watson.current['tags'])))
+ raise click.ClickException(
+ "{} {} {}".format(
+ style("error", "Project already started:"),
+ style("project", watson.current["project"]),
+ style("tags", watson.current["tags"]),
+ )
+ )
frame = get_frame_from_argument(watson, id)
- _start(watson, frame.project, frame.tags, restart=True, start_at=at_,
- gap=gap_)
+ _start(watson, frame.project, frame.tags, restart=True, start_at=at_, gap=gap_)
@cli.command()
@@ -399,20 +458,19 @@ def cancel(watson):
not be recorded.
"""
old = watson.cancel()
- click.echo("Canceling the timer for project {}{}".format(
- style('project', old['project']),
- (" " if old['tags'] else "") + style('tags', old['tags'])
- ))
+ click.echo(
+ "Canceling the timer for project {}{}".format(
+ style("project", old["project"]),
+ (" " if old["tags"] else "") + style("tags", old["tags"]),
+ )
+ )
watson.save()
@cli.command()
-@click.option('-p', '--project', is_flag=True,
- help="only output project")
-@click.option('-t', '--tags', is_flag=True,
- help="only show tags")
-@click.option('-e', '--elapsed', is_flag=True,
- help="only show time elapsed")
+@click.option("-p", "--project", is_flag=True, help="only output project")
+@click.option("-t", "--tags", is_flag=True, help="only show tags")
+@click.option("-e", "--elapsed", is_flag=True, help="only show time elapsed")
@click.pass_obj
@catch_watson_error
def status(watson, project, tags, elapsed):
@@ -442,110 +500,212 @@ def status(watson, project, tags, elapsed):
current = watson.current
if project:
- click.echo("{}".format(
- style('project', current['project']),
- ))
+ click.echo(
+ "{}".format(
+ style("project", current["project"]),
+ )
+ )
return
if tags:
- click.echo("{}".format(
- style('tags', current['tags'])
- ))
+ click.echo("{}".format(style("tags", current["tags"])))
return
if elapsed:
- click.echo("{}".format(
- style('time', current['start'].humanize())
- ))
+ click.echo("{}".format(style("time", current["start"].humanize())))
return
- datefmt = watson.config.get('options', 'date_format', '%Y.%m.%d')
- timefmt = watson.config.get('options', 'time_format', '%H:%M:%S%z')
- click.echo("Project {}{} started {} ({} {})".format(
- style('project', current['project']),
- (" " if current['tags'] else "") + style('tags', current['tags']),
- style('time', current['start'].humanize()),
- style('date', current['start'].strftime(datefmt)),
- style('time', current['start'].strftime(timefmt))
- ))
+ datefmt = watson.config.get("options", "date_format", "%Y.%m.%d")
+ timefmt = watson.config.get("options", "time_format", "%H:%M:%S%z")
+ click.echo(
+ "Project {}{} started {} ({} {})".format(
+ style("project", current["project"]),
+ (" " if current["tags"] else "") + style("tags", current["tags"]),
+ style("time", current["start"].humanize()),
+ style("date", current["start"].strftime(datefmt)),
+ style("time", current["start"].strftime(timefmt)),
+ )
+ )
-_SHORTCUT_OPTIONS = ['all', 'year', 'month', 'luna', 'week', 'day']
-_SHORTCUT_OPTIONS_VALUES = {
- k: get_start_time_for_period(k) for k in _SHORTCUT_OPTIONS
-}
+_SHORTCUT_OPTIONS = ["all", "year", "month", "luna", "week", "day"]
+_SHORTCUT_OPTIONS_VALUES = {k: get_start_time_for_period(k) for k in _SHORTCUT_OPTIONS}
@cli.command()
-@click.option('-c/-C', '--current/--no-current', 'current', default=None,
- help="(Don't) include currently running frame in report.")
-@click.option('-f', '--from', 'from_', cls=MutuallyExclusiveOption,
- type=DateTime, default=arrow.now().shift(days=-7),
- mutually_exclusive=_SHORTCUT_OPTIONS,
- help="The date from when the report should start. Defaults "
- "to seven days ago.")
-@click.option('-t', '--to', cls=MutuallyExclusiveOption, type=DateTime,
- default=arrow.now(),
- mutually_exclusive=_SHORTCUT_OPTIONS,
- help="The date at which the report should stop (inclusive). "
- "Defaults to tomorrow.")
-@click.option('-y', '--year', cls=MutuallyExclusiveOption, type=DateTime,
- flag_value=_SHORTCUT_OPTIONS_VALUES['year'],
- mutually_exclusive=['day', 'week', 'luna', 'month', 'all'],
- help='Reports activity for the current year.')
-@click.option('-m', '--month', cls=MutuallyExclusiveOption, type=DateTime,
- flag_value=_SHORTCUT_OPTIONS_VALUES['month'],
- mutually_exclusive=['day', 'week', 'luna', 'year', 'all'],
- help='Reports activity for the current month.')
-@click.option('-l', '--luna', cls=MutuallyExclusiveOption, type=DateTime,
- flag_value=_SHORTCUT_OPTIONS_VALUES['luna'],
- mutually_exclusive=['day', 'week', 'month', 'year', 'all'],
- help='Reports activity for the current moon cycle.')
-@click.option('-w', '--week', cls=MutuallyExclusiveOption, type=DateTime,
- flag_value=_SHORTCUT_OPTIONS_VALUES['week'],
- mutually_exclusive=['day', 'month', 'luna', 'year', 'all'],
- help='Reports activity for the current week.')
-@click.option('-d', '--day', cls=MutuallyExclusiveOption, type=DateTime,
- flag_value=_SHORTCUT_OPTIONS_VALUES['day'],
- mutually_exclusive=['week', 'month', 'luna', 'year', 'all'],
- help='Reports activity for the current day.')
-@click.option('-a', '--all', cls=MutuallyExclusiveOption, type=DateTime,
- flag_value=_SHORTCUT_OPTIONS_VALUES['all'],
- mutually_exclusive=['day', 'week', 'month', 'luna', 'year'],
- help='Reports all activities.')
-@click.option('-p', '--project', 'projects', shell_complete=get_projects,
- multiple=True,
- help="Reports activity only for the given project. You can add "
- "other projects by using this option several times.")
-@click.option('-T', '--tag', 'tags', shell_complete=get_tags, multiple=True,
- help="Reports activity only for frames containing the given "
- "tag. You can add several tags by using this option multiple "
- "times")
-@click.option('--ignore-project', 'ignore_projects', multiple=True,
- help="Reports activity for all projects but the given ones. You "
- "can ignore several projects by using the option multiple "
- "times. Any given project will be ignored")
-@click.option('--ignore-tag', 'ignore_tags', multiple=True,
- help="Reports activity for all tags but the given ones. You can "
- "ignore several tags by using the option multiple times. Any "
- "given tag will be ignored")
-@click.option('-j', '--json', 'output_format', cls=MutuallyExclusiveOption,
- flag_value='json', mutually_exclusive=['csv'],
- help="Format output in JSON instead of plain text")
-@click.option('-s', '--csv', 'output_format', cls=MutuallyExclusiveOption,
- flag_value='csv', mutually_exclusive=['json'],
- help="Format output in CSV instead of plain text")
-@click.option('--plain', 'output_format', cls=MutuallyExclusiveOption,
- flag_value='plain', mutually_exclusive=['json', 'csv'],
- default=True, hidden=True,
- help="Format output in plain text (default)")
-@click.option('-g/-G', '--pager/--no-pager', 'pager', default=None,
- help="(Don't) view output through a pager.")
+@click.option(
+ "-c/-C",
+ "--current/--no-current",
+ "current",
+ default=None,
+ help="(Don't) include currently running frame in report.",
+)
+@click.option(
+ "-f",
+ "--from",
+ "from_",
+ cls=MutuallyExclusiveOption,
+ type=DateTime,
+ default=arrow.now().shift(days=-7),
+ mutually_exclusive=_SHORTCUT_OPTIONS,
+ help="The date from when the report should start. Defaults " "to seven days ago.",
+)
+@click.option(
+ "-t",
+ "--to",
+ cls=MutuallyExclusiveOption,
+ type=DateTime,
+ default=arrow.now(),
+ mutually_exclusive=_SHORTCUT_OPTIONS,
+ help="The date at which the report should stop (inclusive). "
+ "Defaults to tomorrow.",
+)
+@click.option(
+ "-y",
+ "--year",
+ cls=MutuallyExclusiveOption,
+ type=DateTime,
+ flag_value=_SHORTCUT_OPTIONS_VALUES["year"],
+ mutually_exclusive=["day", "week", "luna", "month", "all"],
+ help="Reports activity for the current year.",
+)
+@click.option(
+ "-m",
+ "--month",
+ cls=MutuallyExclusiveOption,
+ type=DateTime,
+ flag_value=_SHORTCUT_OPTIONS_VALUES["month"],
+ mutually_exclusive=["day", "week", "luna", "year", "all"],
+ help="Reports activity for the current month.",
+)
+@click.option(
+ "-l",
+ "--luna",
+ cls=MutuallyExclusiveOption,
+ type=DateTime,
+ flag_value=_SHORTCUT_OPTIONS_VALUES["luna"],
+ mutually_exclusive=["day", "week", "month", "year", "all"],
+ help="Reports activity for the current moon cycle.",
+)
+@click.option(
+ "-w",
+ "--week",
+ cls=MutuallyExclusiveOption,
+ type=DateTime,
+ flag_value=_SHORTCUT_OPTIONS_VALUES["week"],
+ mutually_exclusive=["day", "month", "luna", "year", "all"],
+ help="Reports activity for the current week.",
+)
+@click.option(
+ "-d",
+ "--day",
+ cls=MutuallyExclusiveOption,
+ type=DateTime,
+ flag_value=_SHORTCUT_OPTIONS_VALUES["day"],
+ mutually_exclusive=["week", "month", "luna", "year", "all"],
+ help="Reports activity for the current day.",
+)
+@click.option(
+ "-a",
+ "--all",
+ cls=MutuallyExclusiveOption,
+ type=DateTime,
+ flag_value=_SHORTCUT_OPTIONS_VALUES["all"],
+ mutually_exclusive=["day", "week", "month", "luna", "year"],
+ help="Reports all activities.",
+)
+@click.option(
+ "-p",
+ "--project",
+ "projects",
+ shell_complete=get_projects,
+ multiple=True,
+ help="Reports activity only for the given project. You can add "
+ "other projects by using this option several times.",
+)
+@click.option(
+ "-T",
+ "--tag",
+ "tags",
+ shell_complete=get_tags,
+ multiple=True,
+ help="Reports activity only for frames containing the given "
+ "tag. You can add several tags by using this option multiple "
+ "times",
+)
+@click.option(
+ "--ignore-project",
+ "ignore_projects",
+ multiple=True,
+ help="Reports activity for all projects but the given ones. You "
+ "can ignore several projects by using the option multiple "
+ "times. Any given project will be ignored",
+)
+@click.option(
+ "--ignore-tag",
+ "ignore_tags",
+ multiple=True,
+ help="Reports activity for all tags but the given ones. You can "
+ "ignore several tags by using the option multiple times. Any "
+ "given tag will be ignored",
+)
+@click.option(
+ "-j",
+ "--json",
+ "output_format",
+ cls=MutuallyExclusiveOption,
+ flag_value="json",
+ mutually_exclusive=["csv"],
+ help="Format output in JSON instead of plain text",
+)
+@click.option(
+ "-s",
+ "--csv",
+ "output_format",
+ cls=MutuallyExclusiveOption,
+ flag_value="csv",
+ mutually_exclusive=["json"],
+ help="Format output in CSV instead of plain text",
+)
+@click.option(
+ "--plain",
+ "output_format",
+ cls=MutuallyExclusiveOption,
+ flag_value="plain",
+ mutually_exclusive=["json", "csv"],
+ default=True,
+ hidden=True,
+ help="Format output in plain text (default)",
+)
+@click.option(
+ "-g/-G",
+ "--pager/--no-pager",
+ "pager",
+ default=None,
+ help="(Don't) view output through a pager.",
+)
@click.pass_obj
@catch_watson_error
-def report(watson, current, from_, to, projects, tags, ignore_projects,
- ignore_tags, year, month, week, day, luna, all, output_format,
- pager, aggregated=False, include_partial_frames=True):
+def report(
+ watson,
+ current,
+ from_,
+ to,
+ projects,
+ tags,
+ ignore_projects,
+ ignore_tags,
+ year,
+ month,
+ week,
+ day,
+ luna,
+ all,
+ output_format,
+ pager,
+ aggregated=False,
+ include_partial_frames=True,
+):
"""
Display a report of the time spent on each project.
@@ -655,36 +815,48 @@ def report(watson, current, from_, to, projects, tags, ignore_projects,
# if the report is an aggregate report, add whitespace using this
# aggregate tab which will be prepended to the project name
if aggregated:
- tab = ' '
+ tab = " "
else:
- tab = ''
-
- report = watson.report(from_, to, current, projects, tags,
- ignore_projects, ignore_tags,
- year=year, month=month, week=week, day=day,
- luna=luna, all=all,
- include_partial_frames=include_partial_frames)
+ tab = ""
+
+ report = watson.report(
+ from_,
+ to,
+ current,
+ projects,
+ tags,
+ ignore_projects,
+ ignore_tags,
+ year=year,
+ month=month,
+ week=week,
+ day=day,
+ luna=luna,
+ all=all,
+ include_partial_frames=include_partial_frames,
+ )
- if 'json' in output_format and not aggregated:
- click.echo(json.dumps(report, indent=4, sort_keys=True,
- default=json_arrow_encoder))
+ if "json" in output_format and not aggregated:
+ click.echo(
+ json.dumps(report, indent=4, sort_keys=True, default=json_arrow_encoder)
+ )
return
- elif 'csv' in output_format and not aggregated:
+ elif "csv" in output_format and not aggregated:
click.echo(build_csv(flatten_report_for_csv(report)))
return
- elif 'plain' not in output_format and aggregated:
+ elif "plain" not in output_format and aggregated:
return report
lines = []
# use the pager, or print directly to the terminal
- if pager or (pager is None and
- watson.config.getboolean('options', 'pager', True)):
+ if pager or (pager is None and watson.config.getboolean("options", "pager", True)):
def _print(line):
lines.append(line)
def _final_print(lines):
- click.echo_via_pager('\n'.join(lines))
+ click.echo_via_pager("\n".join(lines))
+
elif aggregated:
def _print(line):
@@ -692,6 +864,7 @@ def _print(line):
def _final_print(lines):
pass
+
else:
def _print(line):
@@ -702,49 +875,58 @@ def _final_print(lines):
# handle special title formatting for aggregate reports
if aggregated:
- _print('{} - {}'.format(
- style('date', '{:ddd DD MMMM YYYY}'.format(
- report['timespan']['from']
- )),
- style('time', '{}'.format(format_timedelta(
- datetime.timedelta(seconds=report['time'])
- )))
- ))
+ _print(
+ "{} - {}".format(
+ style("date", "{:ddd DD MMMM YYYY}".format(report["timespan"]["from"])),
+ style(
+ "time",
+ "{}".format(
+ format_timedelta(datetime.timedelta(seconds=report["time"]))
+ ),
+ ),
+ )
+ )
else:
- _print('{} -> {}\n'.format(
- style('date', '{:ddd DD MMMM YYYY}'.format(
- report['timespan']['from']
- )),
- style('date', '{:ddd DD MMMM YYYY}'.format(
- report['timespan']['to']
- ))
- ))
+ _print(
+ "{} -> {}\n".format(
+ style("date", "{:ddd DD MMMM YYYY}".format(report["timespan"]["from"])),
+ style("date", "{:ddd DD MMMM YYYY}".format(report["timespan"]["to"])),
+ )
+ )
- projects = report['projects']
+ projects = report["projects"]
for project in projects:
- _print('{tab}{project} - {time}'.format(
- tab=tab,
- time=style('time', format_timedelta(
- datetime.timedelta(seconds=project['time'])
- )),
- project=style('project', project['name'])
- ))
-
- tags = project['tags']
+ _print(
+ "{tab}{project} - {time}".format(
+ tab=tab,
+ time=style(
+ "time",
+ format_timedelta(datetime.timedelta(seconds=project["time"])),
+ ),
+ project=style("project", project["name"]),
+ )
+ )
+
+ tags = project["tags"]
if tags:
- longest_tag = max(len(tag) for tag in tags or [''])
+ longest_tag = max(len(tag) for tag in tags or [""])
for tag in tags:
- _print('\t[{tag} {time}]'.format(
- time=style('time', '{:>11}'.format(format_timedelta(
- datetime.timedelta(seconds=tag['time'])
- ))),
- tag=style('tag', '{:<{}}'.format(
- tag['name'], longest_tag
- )),
- ))
+ _print(
+ "\t[{tag} {time}]".format(
+ time=style(
+ "time",
+ "{:>11}".format(
+ format_timedelta(
+ datetime.timedelta(seconds=tag["time"])
+ )
+ ),
+ ),
+ tag=style("tag", "{:<{}}".format(tag["name"], longest_tag)),
+ )
+ )
_print("")
# if this is a report invoked from `aggregate` return the lines; do not
@@ -752,53 +934,106 @@ def _final_print(lines):
if aggregated:
return lines
- _print('Total: {}'.format(
- style('time', '{}'.format(format_timedelta(
- datetime.timedelta(seconds=report['time'])
- )))
- ))
+ _print(
+ "Total: {}".format(
+ style(
+ "time",
+ "{}".format(
+ format_timedelta(datetime.timedelta(seconds=report["time"]))
+ ),
+ )
+ )
+ )
_final_print(lines)
@cli.command()
-@click.option('-c/-C', '--current/--no-current', 'current', default=None,
- help="(Don't) include currently running frame in report.")
-@click.option('-f', '--from', 'from_', cls=MutuallyExclusiveOption,
- type=DateTime, default=arrow.now().shift(days=-7),
- mutually_exclusive=_SHORTCUT_OPTIONS,
- help="The date from when the report should start. Defaults "
- "to seven days ago.")
-@click.option('-t', '--to', cls=MutuallyExclusiveOption, type=DateTime,
- default=arrow.now(),
- mutually_exclusive=_SHORTCUT_OPTIONS,
- help="The date at which the report should stop (inclusive). "
- "Defaults to tomorrow.")
-@click.option('-p', '--project', 'projects', shell_complete=get_projects,
- multiple=True,
- help="Reports activity only for the given project. You can add "
- "other projects by using this option several times.")
-@click.option('-T', '--tag', 'tags', shell_complete=get_tags, multiple=True,
- help="Reports activity only for frames containing the given "
- "tag. You can add several tags by using this option multiple "
- "times")
-@click.option('-j', '--json', 'output_format', cls=MutuallyExclusiveOption,
- flag_value='json', mutually_exclusive=['csv'],
- help="Format output in JSON instead of plain text")
-@click.option('-s', '--csv', 'output_format', cls=MutuallyExclusiveOption,
- flag_value='csv', mutually_exclusive=['json'],
- help="Format output in CSV instead of plain text")
-@click.option('--plain', 'output_format', cls=MutuallyExclusiveOption,
- flag_value='plain', mutually_exclusive=['json', 'csv'],
- default=True, hidden=True,
- help="Format output in plain text (default)")
-@click.option('-g/-G', '--pager/--no-pager', 'pager', default=None,
- help="(Don't) view output through a pager.")
+@click.option(
+ "-c/-C",
+ "--current/--no-current",
+ "current",
+ default=None,
+ help="(Don't) include currently running frame in report.",
+)
+@click.option(
+ "-f",
+ "--from",
+ "from_",
+ cls=MutuallyExclusiveOption,
+ type=DateTime,
+ default=arrow.now().shift(days=-7),
+ mutually_exclusive=_SHORTCUT_OPTIONS,
+ help="The date from when the report should start. Defaults " "to seven days ago.",
+)
+@click.option(
+ "-t",
+ "--to",
+ cls=MutuallyExclusiveOption,
+ type=DateTime,
+ default=arrow.now(),
+ mutually_exclusive=_SHORTCUT_OPTIONS,
+ help="The date at which the report should stop (inclusive). "
+ "Defaults to tomorrow.",
+)
+@click.option(
+ "-p",
+ "--project",
+ "projects",
+ shell_complete=get_projects,
+ multiple=True,
+ help="Reports activity only for the given project. You can add "
+ "other projects by using this option several times.",
+)
+@click.option(
+ "-T",
+ "--tag",
+ "tags",
+ shell_complete=get_tags,
+ multiple=True,
+ help="Reports activity only for frames containing the given "
+ "tag. You can add several tags by using this option multiple "
+ "times",
+)
+@click.option(
+ "-j",
+ "--json",
+ "output_format",
+ cls=MutuallyExclusiveOption,
+ flag_value="json",
+ mutually_exclusive=["csv"],
+ help="Format output in JSON instead of plain text",
+)
+@click.option(
+ "-s",
+ "--csv",
+ "output_format",
+ cls=MutuallyExclusiveOption,
+ flag_value="csv",
+ mutually_exclusive=["json"],
+ help="Format output in CSV instead of plain text",
+)
+@click.option(
+ "--plain",
+ "output_format",
+ cls=MutuallyExclusiveOption,
+ flag_value="plain",
+ mutually_exclusive=["json", "csv"],
+ default=True,
+ hidden=True,
+ help="Format output in plain text (default)",
+)
+@click.option(
+ "-g/-G",
+ "--pager/--no-pager",
+ "pager",
+ default=None,
+ help="(Don't) view output through a pager.",
+)
@click.pass_obj
@click.pass_context
@catch_watson_error
-def aggregate(ctx, watson, current, from_, to, projects, tags, output_format,
- pager):
+def aggregate(ctx, watson, current, from_, to, projects, tags, output_format, pager):
"""
Display a report of the time spent on each project aggregated by day.
@@ -874,104 +1109,220 @@ def aggregate(ctx, watson, current, from_, to, projects, tags, output_format,
for i in range(delta + 1):
offset = datetime.timedelta(days=i)
from_offset = from_ + offset
- output = ctx.invoke(report, current=current, from_=from_offset,
- to=from_offset, projects=projects, tags=tags,
- output_format=output_format,
- pager=pager, aggregated=True,
- include_partial_frames=True)
+ output = ctx.invoke(
+ report,
+ current=current,
+ from_=from_offset,
+ to=from_offset,
+ projects=projects,
+ tags=tags,
+ output_format=output_format,
+ pager=pager,
+ aggregated=True,
+ include_partial_frames=True,
+ )
- if 'json' in output_format:
+ if "json" in output_format:
lines.append(output)
- elif 'csv' in output_format:
+ elif "csv" in output_format:
lines.extend(flatten_report_for_csv(output))
else:
# if there is no activity for the day, append a newline
# this ensures even spacing throughout the report
if (len(output)) == 1:
- output[0] += '\n'
+ output[0] += "\n"
- lines.append('\n'.join(output))
+ lines.append("\n".join(output))
- if 'json' in output_format:
- click.echo(json.dumps(lines, indent=4, sort_keys=True,
- default=json_arrow_encoder))
- elif 'csv' in output_format:
+ if "json" in output_format:
+ click.echo(
+ json.dumps(lines, indent=4, sort_keys=True, default=json_arrow_encoder)
+ )
+ elif "csv" in output_format:
click.echo(build_csv(lines))
- elif pager or (pager is None and
- watson.config.getboolean('options', 'pager', True)):
- click.echo_via_pager('\n\n'.join(lines))
+ elif pager or (
+ pager is None and watson.config.getboolean("options", "pager", True)
+ ):
+ click.echo_via_pager("\n\n".join(lines))
else:
- click.echo('\n\n'.join(lines))
+ click.echo("\n\n".join(lines))
@cli.command()
-@click.option('-c/-C', '--current/--no-current', 'current', default=None,
- help="(Don't) include currently running frame in output.")
-@click.option('-r/-R', '--reverse/--no-reverse', 'reverse', default=None,
- help="(Don't) reverse the order of the days in output.")
-@click.option('-f', '--from', 'from_', type=DateTime,
- default=arrow.now().shift(days=-7),
- help="The date from when the log should start. Defaults "
- "to seven days ago.")
-@click.option('-t', '--to', type=DateTime, default=arrow.now(),
- help="The date at which the log should stop (inclusive). "
- "Defaults to tomorrow.")
-@click.option('-y', '--year', cls=MutuallyExclusiveOption, type=DateTime,
- flag_value=_SHORTCUT_OPTIONS_VALUES['year'],
- mutually_exclusive=['day', 'week', 'month', 'all'],
- help='Reports activity for the current year.')
-@click.option('-m', '--month', cls=MutuallyExclusiveOption, type=DateTime,
- flag_value=_SHORTCUT_OPTIONS_VALUES['month'],
- mutually_exclusive=['day', 'week', 'year', 'all'],
- help='Reports activity for the current month.')
-@click.option('-l', '--luna', cls=MutuallyExclusiveOption, type=DateTime,
- flag_value=_SHORTCUT_OPTIONS_VALUES['luna'],
- mutually_exclusive=['day', 'week', 'month', 'year', 'all'],
- help='Reports activity for the current moon cycle.')
-@click.option('-w', '--week', cls=MutuallyExclusiveOption, type=DateTime,
- flag_value=_SHORTCUT_OPTIONS_VALUES['week'],
- mutually_exclusive=['day', 'month', 'year', 'all'],
- help='Reports activity for the current week.')
-@click.option('-d', '--day', cls=MutuallyExclusiveOption, type=DateTime,
- flag_value=_SHORTCUT_OPTIONS_VALUES['day'],
- mutually_exclusive=['week', 'month', 'year', 'all'],
- help='Reports activity for the current day.')
-@click.option('-a', '--all', cls=MutuallyExclusiveOption, type=DateTime,
- flag_value=_SHORTCUT_OPTIONS_VALUES['all'],
- mutually_exclusive=['day', 'week', 'month', 'year'],
- help='Reports all activities.')
-@click.option('-p', '--project', 'projects', shell_complete=get_projects,
- multiple=True,
- help="Logs activity only for the given project. You can add "
- "other projects by using this option several times.")
-@click.option('-T', '--tag', 'tags', shell_complete=get_tags, multiple=True,
- help="Logs activity only for frames containing the given "
- "tag. You can add several tags by using this option multiple "
- "times")
-@click.option('--ignore-project', 'ignore_projects', multiple=True,
- help="Logs activity for all projects but the given ones. You "
- "can ignore several projects by using the option multiple "
- "times. Any given project will be ignored")
-@click.option('--ignore-tag', 'ignore_tags', multiple=True,
- help="Logs activity for all tags but the given ones. You can "
- "ignore several tags by using the option multiple times. Any "
- "given tag will be ignored")
-@click.option('-j', '--json', 'output_format', cls=MutuallyExclusiveOption,
- flag_value='json', mutually_exclusive=['csv'],
- help="Format output in JSON instead of plain text")
-@click.option('-s', '--csv', 'output_format', cls=MutuallyExclusiveOption,
- flag_value='csv', mutually_exclusive=['json'],
- help="Format output in CSV instead of plain text")
-@click.option('--plain', 'output_format', cls=MutuallyExclusiveOption,
- flag_value='plain', mutually_exclusive=['json', 'csv'],
- default=True, hidden=True,
- help="Format output in plain text (default)")
-@click.option('-g/-G', '--pager/--no-pager', 'pager', default=None,
- help="(Don't) view output through a pager.")
+@click.option(
+ "-c/-C",
+ "--current/--no-current",
+ "current",
+ default=None,
+ help="(Don't) include currently running frame in output.",
+)
+@click.option(
+ "-r/-R",
+ "--reverse/--no-reverse",
+ "reverse",
+ default=None,
+ help="(Don't) reverse the order of the days in output.",
+)
+@click.option(
+ "-f",
+ "--from",
+ "from_",
+ type=DateTime,
+ default=arrow.now().shift(days=-7),
+ help="The date from when the log should start. Defaults " "to seven days ago.",
+)
+@click.option(
+ "-t",
+ "--to",
+ type=DateTime,
+ default=arrow.now(),
+ help="The date at which the log should stop (inclusive). " "Defaults to tomorrow.",
+)
+@click.option(
+ "-y",
+ "--year",
+ cls=MutuallyExclusiveOption,
+ type=DateTime,
+ flag_value=_SHORTCUT_OPTIONS_VALUES["year"],
+ mutually_exclusive=["day", "week", "month", "all"],
+ help="Reports activity for the current year.",
+)
+@click.option(
+ "-m",
+ "--month",
+ cls=MutuallyExclusiveOption,
+ type=DateTime,
+ flag_value=_SHORTCUT_OPTIONS_VALUES["month"],
+ mutually_exclusive=["day", "week", "year", "all"],
+ help="Reports activity for the current month.",
+)
+@click.option(
+ "-l",
+ "--luna",
+ cls=MutuallyExclusiveOption,
+ type=DateTime,
+ flag_value=_SHORTCUT_OPTIONS_VALUES["luna"],
+ mutually_exclusive=["day", "week", "month", "year", "all"],
+ help="Reports activity for the current moon cycle.",
+)
+@click.option(
+ "-w",
+ "--week",
+ cls=MutuallyExclusiveOption,
+ type=DateTime,
+ flag_value=_SHORTCUT_OPTIONS_VALUES["week"],
+ mutually_exclusive=["day", "month", "year", "all"],
+ help="Reports activity for the current week.",
+)
+@click.option(
+ "-d",
+ "--day",
+ cls=MutuallyExclusiveOption,
+ type=DateTime,
+ flag_value=_SHORTCUT_OPTIONS_VALUES["day"],
+ mutually_exclusive=["week", "month", "year", "all"],
+ help="Reports activity for the current day.",
+)
+@click.option(
+ "-a",
+ "--all",
+ cls=MutuallyExclusiveOption,
+ type=DateTime,
+ flag_value=_SHORTCUT_OPTIONS_VALUES["all"],
+ mutually_exclusive=["day", "week", "month", "year"],
+ help="Reports all activities.",
+)
+@click.option(
+ "-p",
+ "--project",
+ "projects",
+ shell_complete=get_projects,
+ multiple=True,
+ help="Logs activity only for the given project. You can add "
+ "other projects by using this option several times.",
+)
+@click.option(
+ "-T",
+ "--tag",
+ "tags",
+ shell_complete=get_tags,
+ multiple=True,
+ help="Logs activity only for frames containing the given "
+ "tag. You can add several tags by using this option multiple "
+ "times",
+)
+@click.option(
+ "--ignore-project",
+ "ignore_projects",
+ multiple=True,
+ help="Logs activity for all projects but the given ones. You "
+ "can ignore several projects by using the option multiple "
+ "times. Any given project will be ignored",
+)
+@click.option(
+ "--ignore-tag",
+ "ignore_tags",
+ multiple=True,
+ help="Logs activity for all tags but the given ones. You can "
+ "ignore several tags by using the option multiple times. Any "
+ "given tag will be ignored",
+)
+@click.option(
+ "-j",
+ "--json",
+ "output_format",
+ cls=MutuallyExclusiveOption,
+ flag_value="json",
+ mutually_exclusive=["csv"],
+ help="Format output in JSON instead of plain text",
+)
+@click.option(
+ "-s",
+ "--csv",
+ "output_format",
+ cls=MutuallyExclusiveOption,
+ flag_value="csv",
+ mutually_exclusive=["json"],
+ help="Format output in CSV instead of plain text",
+)
+@click.option(
+ "--plain",
+ "output_format",
+ cls=MutuallyExclusiveOption,
+ flag_value="plain",
+ mutually_exclusive=["json", "csv"],
+ default=True,
+ hidden=True,
+ help="Format output in plain text (default)",
+)
+@click.option(
+ "-g/-G",
+ "--pager/--no-pager",
+ "pager",
+ default=None,
+ help="(Don't) view output through a pager.",
+)
@click.pass_obj
@catch_watson_error
-def log(watson, current, reverse, from_, to, projects, tags, ignore_projects,
- ignore_tags, year, month, week, day, luna, all, output_format, pager):
+def log(
+ watson,
+ current,
+ reverse,
+ from_,
+ to,
+ projects,
+ tags,
+ ignore_projects,
+ ignore_tags,
+ year,
+ month,
+ week,
+ day,
+ luna,
+ all,
+ output_format,
+ pager,
+):
"""
Display each recorded session during the given timespan.
@@ -1035,63 +1386,65 @@ def log(watson, current, reverse, from_, to, projects, tags, ignore_projects,
02cb269,2014-04-16 09:53,2014-04-16 12:43,apollo11,wheels
1070ddb,2014-04-16 13:48,2014-04-16 16:17,voyager1,"antenna, sensors"
""" # noqa
- for start_time in (_ for _ in [day, week, month, luna, year, all]
- if _ is not None):
+ for start_time in (_ for _ in [day, week, month, luna, year, all] if _ is not None):
from_ = start_time
if from_ > to:
raise click.ClickException("'from' must be anterior to 'to'")
- if bool(projects and ignore_projects and
- set(projects).intersection(set(ignore_projects))):
- raise click.ClickException(
- "given projects can't be ignored at the same time")
+ if bool(
+ projects
+ and ignore_projects
+ and set(projects).intersection(set(ignore_projects))
+ ):
+ raise click.ClickException("given projects can't be ignored at the same time")
if bool(tags and ignore_tags and set(tags).intersection(set(ignore_tags))):
- raise click.ClickException(
- "given tags can't be ignored at the same time")
+ raise click.ClickException("given tags can't be ignored at the same time")
if watson.current:
- if current or (current is None and
- watson.config.getboolean('options', 'log_current')):
+ if current or (
+ current is None and watson.config.getboolean("options", "log_current")
+ ):
cur = watson.current
- watson.frames.add(cur['project'], cur['start'], arrow.utcnow(),
- cur['tags'], id="current")
+ watson.frames.add(
+ cur["project"], cur["start"], arrow.utcnow(), cur["tags"], id="current"
+ )
if reverse is None:
- reverse = watson.config.getboolean('options', 'reverse_log', True)
+ reverse = watson.config.getboolean("options", "reverse_log", True)
span = watson.frames.span(from_, to)
filtered_frames = watson.frames.filter(
- projects=projects or None, tags=tags or None,
+ projects=projects or None,
+ tags=tags or None,
ignore_projects=ignore_projects or None,
- ignore_tags=ignore_tags or None, span=span
+ ignore_tags=ignore_tags or None,
+ span=span,
)
- if 'json' in output_format:
+ if "json" in output_format:
click.echo(frames_to_json(filtered_frames))
return
- if 'csv' in output_format:
+ if "csv" in output_format:
click.echo(frames_to_csv(filtered_frames))
return
frames_by_day = sorted_groupby(
- filtered_frames,
- operator.attrgetter('day'),
- reverse=reverse
+ filtered_frames, operator.attrgetter("day"), reverse=reverse
)
lines = []
# use the pager, or print directly to the terminal
- if pager or (pager is None and
- watson.config.getboolean('options', 'pager', True)):
+ if pager or (pager is None and watson.config.getboolean("options", "pager", True)):
def _print(line):
lines.append(line)
def _final_print(lines):
- click.echo_via_pager('\n'.join(lines))
+ click.echo_via_pager("\n".join(lines))
+
else:
def _print(line):
@@ -1102,36 +1455,37 @@ def _final_print(lines):
for i, (day, frames) in enumerate(frames_by_day):
if i != 0:
- _print('')
+ _print("")
- frames = sorted(frames, key=operator.attrgetter('start'))
+ frames = sorted(frames, key=operator.attrgetter("start"))
longest_project = max(len(frame.project) for frame in frames)
daily_total = reduce(
- operator.add,
- (frame.stop - frame.start for frame in frames)
+ operator.add, (frame.stop - frame.start for frame in frames)
)
_print(
"{date} ({daily_total})".format(
- date=style('date', "{:dddd DD MMMM YYYY}".format(day)),
- daily_total=style('time', format_timedelta(daily_total))
+ date=style("date", "{:dddd DD MMMM YYYY}".format(day)),
+ daily_total=style("time", format_timedelta(daily_total)),
)
)
- _print("\n".join(
- "\t{id} {start} to {stop} {delta:>11} {project}{tags}".format(
- delta=format_timedelta(frame.stop - frame.start),
- project=style('project', '{:>{}}'.format(
- frame.project, longest_project
- )),
- tags=(" "*2 if frame.tags else "") + style('tags', frame.tags),
- start=style('time', '{:HH:mm}'.format(frame.start)),
- stop=style('time', '{:HH:mm}'.format(frame.stop)),
- id=style('short_id', frame.id)
+ _print(
+ "\n".join(
+ "\t{id} {start} to {stop} {delta:>11} {project}{tags}".format(
+ delta=format_timedelta(frame.stop - frame.start),
+ project=style(
+ "project", "{:>{}}".format(frame.project, longest_project)
+ ),
+ tags=(" " * 2 if frame.tags else "") + style("tags", frame.tags),
+ start=style("time", "{:HH:mm}".format(frame.start)),
+ stop=style("time", "{:HH:mm}".format(frame.stop)),
+ id=style("short_id", frame.id),
+ )
+ for frame in frames
)
- for frame in frames
- ))
+ )
_final_print(lines)
@@ -1153,7 +1507,7 @@ def projects(watson):
voyager2
"""
for project in watson.projects:
- click.echo(style('project', project))
+ click.echo(style("project", project))
@cli.command()
@@ -1181,7 +1535,7 @@ def tags(watson):
wheels
"""
for tag in watson.tags:
- click.echo(style('tag', tag))
+ click.echo(style("tag", tag))
@cli.command()
@@ -1201,20 +1555,40 @@ def frames(watson):
[...]
"""
for frame in watson.frames:
- click.echo(style('short_id', frame.id))
-
-
-@cli.command(context_settings={'ignore_unknown_options': True})
-@click.argument('args', nargs=-1,
- shell_complete=get_project_or_task_completion)
-@click.option('-f', '--from', 'from_', required=True, type=DateTime,
- help="Date and time of start of tracked activity")
-@click.option('-t', '--to', required=True, type=DateTime,
- help="Date and time of end of tracked activity")
-@click.option('-c', '--confirm-new-project', is_flag=True, default=False,
- help="Confirm addition of new project.")
-@click.option('-b', '--confirm-new-tag', is_flag=True, default=False,
- help="Confirm creation of new tag.")
+ click.echo(style("short_id", frame.id))
+
+
+@cli.command(context_settings={"ignore_unknown_options": True})
+@click.argument("args", nargs=-1, shell_complete=get_project_or_task_completion)
+@click.option(
+ "-f",
+ "--from",
+ "from_",
+ required=True,
+ type=DateTime,
+ help="Date and time of start of tracked activity",
+)
+@click.option(
+ "-t",
+ "--to",
+ required=True,
+ type=DateTime,
+ help="Date and time of end of tracked activity",
+)
+@click.option(
+ "-c",
+ "--confirm-new-project",
+ is_flag=True,
+ default=False,
+ help="Confirm addition of new project.",
+)
+@click.option(
+ "-b",
+ "--confirm-new-tag",
+ is_flag=True,
+ default=False,
+ help="Confirm creation of new tag.",
+)
@click.pass_obj
@catch_watson_error
def add(watson, args, from_, to, confirm_new_project, confirm_new_tag):
@@ -1228,45 +1602,54 @@ def add(watson, args, from_, to, confirm_new_project, confirm_new_tag):
programming +addfeature
"""
# parse project name from args
- project = ' '.join(
- itertools.takewhile(lambda s: not s.startswith('+'), args)
- )
+ project = " ".join(itertools.takewhile(lambda s: not s.startswith("+"), args))
if not project:
raise click.ClickException("No project given.")
# Confirm creation of new project if that option is set
- if (watson.config.getboolean('options', 'confirm_new_project') or
- confirm_new_project):
+ if (
+ watson.config.getboolean("options", "confirm_new_project")
+ or confirm_new_project
+ ):
confirm_project(project, watson.projects)
# Parse all the tags
tags = parse_tags(args)
# Confirm creation of new tag(s) if that option is set
- if (watson.config.getboolean('options', 'confirm_new_tag') or
- confirm_new_tag):
+ if watson.config.getboolean("options", "confirm_new_tag") or confirm_new_tag:
confirm_tags(tags, watson.tags)
# add a new frame, call watson save to update state files
frame = watson.add(project=project, tags=tags, from_date=from_, to_date=to)
click.echo(
"Adding project {}{}, started {} and stopped {}. (id: {})".format(
- style('project', frame.project),
- (" " if frame.tags else "") + style('tags', frame.tags),
- style('time', frame.start.humanize()),
- style('time', frame.stop.humanize()),
- style('short_id', frame.id)
+ style("project", frame.project),
+ (" " if frame.tags else "") + style("tags", frame.tags),
+ style("time", frame.start.humanize()),
+ style("time", frame.stop.humanize()),
+ style("short_id", frame.id),
)
)
watson.save()
-@cli.command(context_settings={'ignore_unknown_options': True})
-@click.option('-c', '--confirm-new-project', is_flag=True, default=False,
- help="Confirm addition of new project.")
-@click.option('-b', '--confirm-new-tag', is_flag=True, default=False,
- help="Confirm creation of new tag.")
-@click.argument('id', required=False, shell_complete=get_frames)
+@cli.command(context_settings={"ignore_unknown_options": True})
+@click.option(
+ "-c",
+ "--confirm-new-project",
+ is_flag=True,
+ default=False,
+ help="Confirm addition of new project.",
+)
+@click.option(
+ "-b",
+ "--confirm-new-tag",
+ is_flag=True,
+ default=False,
+ help="Confirm creation of new tag.",
+)
+@click.argument("id", required=False, shell_complete=get_frames)
@click.pass_obj
@catch_watson_error
def edit(watson, confirm_new_project, confirm_new_tag, id):
@@ -1284,33 +1667,41 @@ def edit(watson, confirm_new_project, confirm_new_tag, id):
variables (in that order) and defaults to `notepad` on Windows systems and
to `vim`, `nano`, or `vi` (first one found) on all other systems.
"""
- date_format = 'YYYY-MM-DD'
- time_format = 'HH:mm:ss'
- datetime_format = '{} {}'.format(date_format, time_format)
+ date_format = "YYYY-MM-DD"
+ time_format = "HH:mm:ss"
+ datetime_format = "{} {}".format(date_format, time_format)
local_tz = local_tz_info()
if id:
frame = get_frame_from_argument(watson, id)
id = frame.id
elif watson.is_started:
- frame = Frame(watson.current['start'], None, watson.current['project'],
- None, watson.current['tags'])
+ frame = Frame(
+ watson.current["start"],
+ None,
+ watson.current["project"],
+ None,
+ watson.current["tags"],
+ )
elif watson.frames:
frame = watson.frames[-1]
id = frame.id
else:
raise click.ClickException(
- style('error', "No frames recorded yet. It's time to create your "
- "first one!"))
+ style(
+ "error",
+ "No frames recorded yet. It's time to create your " "first one!",
+ )
+ )
data = {
- 'start': frame.start.format(datetime_format),
- 'project': frame.project,
- 'tags': frame.tags,
+ "start": frame.start.format(datetime_format),
+ "project": frame.project,
+ "tags": frame.tags,
}
if id:
- data['stop'] = frame.stop.format(datetime_format)
+ data["stop"] = frame.stop.format(datetime_format)
text = json.dumps(data, indent=4, sort_keys=True, ensure_ascii=False)
@@ -1320,7 +1711,7 @@ def edit(watson, confirm_new_project, confirm_new_tag, id):
# enter into while loop until successful and validated
# edit has been performed
while True:
- output = click.edit(text, extension='.json')
+ output = click.edit(text, extension=".json")
if not output:
click.echo("No change made.")
@@ -1328,25 +1719,36 @@ def edit(watson, confirm_new_project, confirm_new_tag, id):
try:
data = json.loads(output)
- project = data['project']
+ project = data["project"]
# Confirm creation of new project if that option is set
- if (watson.config.getboolean('options', 'confirm_new_project') or
- confirm_new_project):
+ if (
+ watson.config.getboolean("options", "confirm_new_project")
+ or confirm_new_project
+ ):
confirm_project(project, watson.projects)
- tags = data['tags']
+ tags = data["tags"]
# Confirm creation of new tag(s) if that option is set
- if (watson.config.getboolean('options', 'confirm_new_tag') or
- confirm_new_tag):
+ if (
+ watson.config.getboolean("options", "confirm_new_tag")
+ or confirm_new_tag
+ ):
confirm_tags(tags, watson.tags)
- start = arrow.get(data['start'], datetime_format).replace(
- tzinfo=local_tz).to('utc')
- stop = arrow.get(data['stop'], datetime_format).replace(
- tzinfo=local_tz).to('utc') if id else None
+ start = (
+ arrow.get(data["start"], datetime_format)
+ .replace(tzinfo=local_tz)
+ .to("utc")
+ )
+ stop = (
+ arrow.get(data["stop"], datetime_format)
+ .replace(tzinfo=local_tz)
+ .to("utc")
+ if id
+ else None
+ )
# if start time of the project is not before end time
# raise ValueException
if not watson.is_started and start > stop:
- raise ValueError(
- "Task cannot end before it starts.")
+ raise ValueError("Task cannot end before it starts.")
if start > arrow.utcnow():
raise ValueError("Start time cannot be in the future")
if stop and stop > arrow.utcnow():
@@ -1355,12 +1757,12 @@ def edit(watson, confirm_new_project, confirm_new_tag, id):
# the edit function normally
break
except (ValueError, TypeError, RuntimeError) as e:
- click.echo("Error while parsing inputted values: {}".format(e),
- err=True)
+ click.echo("Error while parsing inputted values: {}".format(e), err=True)
except KeyError:
click.echo(
- "The edited frame must contain the project, "
- "start, and stop keys.", err=True)
+ "The edited frame must contain the project, " "start, and stop keys.",
+ err=True,
+ )
# we reach here if exception was thrown, wait for user
# to acknowledge the error before looping in while and
# showing user the editor again
@@ -1379,25 +1781,18 @@ def edit(watson, confirm_new_project, confirm_new_tag, id):
click.echo(
"Edited frame for project {project}{tags}, from {start} to {stop} "
"({delta})".format(
- delta=format_timedelta(stop - start) if stop else '-',
- project=style('project', project),
- tags=(" " if tags else "") + style('tags', tags),
- start=style(
- 'time',
- start.to(local_tz).format(time_format)
- ),
- stop=style(
- 'time',
- stop.to(local_tz).format(time_format) if stop else '-'
- )
+ delta=format_timedelta(stop - start) if stop else "-",
+ project=style("project", project),
+ tags=(" " if tags else "") + style("tags", tags),
+ start=style("time", start.to(local_tz).format(time_format)),
+ stop=style("time", stop.to(local_tz).format(time_format) if stop else "-"),
)
)
-@cli.command(context_settings={'ignore_unknown_options': True})
-@click.argument('id', shell_complete=get_frames)
-@click.option('-f', '--force', is_flag=True,
- help="Don't ask for confirmation.")
+@cli.command(context_settings={"ignore_unknown_options": True})
+@click.argument("id", shell_complete=get_frames)
+@click.option("-f", "--force", is_flag=True, help="Don't ask for confirmation.")
@click.pass_obj
@catch_watson_error
def remove(watson, id, force):
@@ -1412,12 +1807,12 @@ def remove(watson, id, force):
click.confirm(
"You are about to remove frame "
"{project}{tags} from {start} to {stop}, continue?".format(
- project=style('project', frame.project),
- tags=(" " if frame.tags else "") + style('tags', frame.tags),
- start=style('time', '{:HH:mm}'.format(frame.start)),
- stop=style('time', '{:HH:mm}'.format(frame.stop))
+ project=style("project", frame.project),
+ tags=(" " if frame.tags else "") + style("tags", frame.tags),
+ start=style("time", "{:HH:mm}".format(frame.start)),
+ stop=style("time", "{:HH:mm}".format(frame.stop)),
),
- abort=True
+ abort=True,
)
del watson.frames[id]
@@ -1427,10 +1822,11 @@ def remove(watson, id, force):
@cli.command()
-@click.argument('key', required=False, metavar='SECTION.OPTION')
-@click.argument('value', required=False)
-@click.option('-e', '--edit', is_flag=True,
- help="Edit the configuration file with an editor.")
+@click.argument("key", required=False, metavar="SECTION.OPTION")
+@click.argument("value", required=False)
+@click.option(
+ "-e", "--edit", is_flag=True, help="Edit the configuration file with an editor."
+)
@click.pass_context
@catch_watson_error
def config(context, key, value, edit):
@@ -1457,9 +1853,9 @@ def config(context, key, value, edit):
with open(watson.config_file) as fp:
rawconfig = fp.read()
except (IOError, OSError):
- rawconfig = ''
+ rawconfig = ""
- newconfig = click.edit(text=rawconfig, extension='.ini')
+ newconfig = click.edit(text=rawconfig, extension=".ini")
if newconfig:
safe_save(watson.config_file, newconfig)
@@ -1470,7 +1866,7 @@ def config(context, key, value, edit):
except _watson.ConfigurationError as exc:
watson.config = wconfig
watson.save()
- raise click.ClickException(style('error', str(exc)))
+ raise click.ClickException(style("error", str(exc)))
return
if not key:
@@ -1478,11 +1874,9 @@ def config(context, key, value, edit):
return
try:
- section, option = key.split('.')
+ section, option = key.split(".")
except ValueError:
- raise click.ClickException(
- "The key must have the format 'section.option'"
- )
+ raise click.ClickException("The key must have the format 'section.option'")
if value is None:
if not wconfig.has_section(section):
@@ -1534,10 +1928,14 @@ def sync(watson):
@cli.command()
-@click.argument('frames_with_conflict', type=click.Path(exists=True))
-@click.option('-f', '--force', 'force', is_flag=True,
- help="If specified, then the merge will automatically "
- "be performed.")
+@click.argument("frames_with_conflict", type=click.Path(exists=True))
+@click.option(
+ "-f",
+ "--force",
+ "force",
+ is_flag=True,
+ help="If specified, then the merge will automatically " "be performed.",
+)
@click.pass_obj
@catch_watson_error
def merge(watson, frames_with_conflict, force):
@@ -1604,12 +2002,17 @@ def merge(watson, frames_with_conflict, force):
# digits of this length
dig = len(str(max(len(original_frames), len(merging), len(conflicting))))
- click.echo("{:<{width}} frames will be left unchanged".format(
- len(original_frames) - len(conflicting), width=dig))
- click.echo("{:<{width}} frames will be merged".format(
- len(merging), width=dig))
- click.echo("{:<{width}} frames will need to be resolved".format(
- len(conflicting), width=dig))
+ click.echo(
+ "{:<{width}} frames will be left unchanged".format(
+ len(original_frames) - len(conflicting), width=dig
+ )
+ )
+ click.echo("{:<{width}} frames will be merged".format(len(merging), width=dig))
+ click.echo(
+ "{:<{width}} frames will need to be resolved".format(
+ len(conflicting), width=dig
+ )
+ )
# No frames to resolve or merge.
if not conflicting and not merging:
@@ -1622,21 +2025,29 @@ def merge(watson, frames_with_conflict, force):
if conflicting:
click.echo("Will resolve conflicts:")
- date_format = 'YYYY-MM-DD HH:mm:ss'
+ date_format = "YYYY-MM-DD HH:mm:ss"
for conflict_frame in conflicting:
original_frame = original_frames[conflict_frame.id]
# Print original frame
original_frame_data = {
- 'project': original_frame.project,
- 'start': original_frame.start.format(date_format),
- 'stop': original_frame.stop.format(date_format),
- 'tags': original_frame.tags
+ "project": original_frame.project,
+ "start": original_frame.start.format(date_format),
+ "stop": original_frame.stop.format(date_format),
+ "tags": original_frame.tags,
}
- click.echo("frame {}:".format(style('short_id', original_frame.id)))
- click.echo("{}".format('\n'.join('<' + line for line in json.dumps(
- original_frame_data, indent=4, ensure_ascii=False).splitlines())))
+ click.echo("frame {}:".format(style("short_id", original_frame.id)))
+ click.echo(
+ "{}".format(
+ "\n".join(
+ "<" + line
+ for line in json.dumps(
+ original_frame_data, indent=4, ensure_ascii=False
+ ).splitlines()
+ )
+ )
+ )
click.echo("---")
# make a copy of the namedtuple
@@ -1644,43 +2055,53 @@ def merge(watson, frames_with_conflict, force):
# highlight conflicts
if conflict_frame.project != original_frame.project:
- project = '**' + str(conflict_frame.project) + '**'
+ project = "**" + str(conflict_frame.project) + "**"
conflict_frame_copy = conflict_frame_copy._replace(project=project)
if conflict_frame.start != original_frame.start:
- start = '**' + str(conflict_frame.start.format(date_format)) + '**'
+ start = "**" + str(conflict_frame.start.format(date_format)) + "**"
conflict_frame_copy = conflict_frame_copy._replace(start=start)
if conflict_frame.stop != original_frame.stop:
- stop = '**' + str(conflict_frame.stop.format(date_format)) + '**'
+ stop = "**" + str(conflict_frame.stop.format(date_format)) + "**"
conflict_frame_copy = conflict_frame_copy._replace(stop=stop)
for idx, tag in enumerate(conflict_frame.tags):
if tag not in original_frame.tags:
- conflict_frame_copy.tags[idx] = '**' + str(tag) + '**'
+ conflict_frame_copy.tags[idx] = "**" + str(tag) + "**"
# Print conflicting frame
conflict_frame_data = {
- 'project': conflict_frame_copy.project,
- 'start': conflict_frame_copy.start.format(date_format),
- 'stop': conflict_frame_copy.stop.format(date_format),
- 'tags': conflict_frame_copy.tags
+ "project": conflict_frame_copy.project,
+ "start": conflict_frame_copy.start.format(date_format),
+ "stop": conflict_frame_copy.stop.format(date_format),
+ "tags": conflict_frame_copy.tags,
}
- click.echo("{}".format('\n'.join('>' + line for line in json.dumps(
- conflict_frame_data, indent=4, ensure_ascii=False).splitlines())))
+ click.echo(
+ "{}".format(
+ "\n".join(
+ ">" + line
+ for line in json.dumps(
+ conflict_frame_data, indent=4, ensure_ascii=False
+ ).splitlines()
+ )
+ )
+ )
resp = click.prompt(
"Select the frame you want to keep: left or right? (L/r)",
- value_proc=options(['L', 'r']))
+ value_proc=options(["L", "r"]),
+ )
- if resp == 'r':
+ if resp == "r":
# replace original frame with conflicting frame
original_frames[conflict_frame.id] = conflict_frame
# merge in any non-conflicting frames
for frame in merging:
start, stop, project, id, tags, updated_at = frame.dump()
- original_frames.add(project, start, stop, tags=tags, id=id,
- updated_at=updated_at)
+ original_frames.add(
+ project, start, stop, tags=tags, id=id, updated_at=updated_at
+ )
watson.frames = original_frames
watson.frames.changed = True
@@ -1688,10 +2109,11 @@ def merge(watson, frames_with_conflict, force):
@cli.command()
-@click.argument('rename_type', required=True, metavar='TYPE',
- shell_complete=get_rename_types)
-@click.argument('old_name', required=True, shell_complete=get_rename_name)
-@click.argument('new_name', required=True, shell_complete=get_rename_name)
+@click.argument(
+ "rename_type", required=True, metavar="TYPE", shell_complete=get_rename_types
+)
+@click.argument("old_name", required=True, shell_complete=get_rename_name)
+@click.argument("new_name", required=True, shell_complete=get_rename_name)
@click.pass_obj
@catch_watson_error
def rename(watson, rename_type, old_name, new_name):
@@ -1707,21 +2129,25 @@ def rename(watson, rename_type, old_name, new_name):
Renamed tag "company-meeting" to "meeting"
"""
- if rename_type == 'tag':
+ if rename_type == "tag":
watson.rename_tag(old_name, new_name)
- click.echo('Renamed tag "{}" to "{}"'.format(
- style('tag', old_name),
- style('tag', new_name)
- ))
- elif rename_type == 'project':
+ click.echo(
+ 'Renamed tag "{}" to "{}"'.format(
+ style("tag", old_name), style("tag", new_name)
+ )
+ )
+ elif rename_type == "project":
watson.rename_project(old_name, new_name)
- click.echo('Renamed project "{}" to "{}"'.format(
- style('project', old_name),
- style('project', new_name)
- ))
+ click.echo(
+ 'Renamed project "{}" to "{}"'.format(
+ style("project", old_name), style("project", new_name)
+ )
+ )
else:
- raise click.ClickException(style(
- 'error',
- 'You have to call rename with type "project" or "tag"; '
- 'you supplied "%s"' % rename_type
- ))
+ raise click.ClickException(
+ style(
+ "error",
+ 'You have to call rename with type "project" or "tag"; '
+ 'you supplied "%s"' % rename_type,
+ )
+ )
diff --git a/watson/config.py b/watson/config.py
index bd3d61f8..221d033c 100644
--- a/watson/config.py
+++ b/watson/config.py
@@ -3,7 +3,7 @@
import shlex
from configparser import RawConfigParser
-__all__ = ('ConfigParser',)
+__all__ = ("ConfigParser",)
class ConfigParser(RawConfigParser):
@@ -16,8 +16,11 @@ def get(self, section, option, default=None, **kwargs):
If option is not set, return default instead (defaults to None).
"""
- return (RawConfigParser.get(self, section, option, **kwargs)
- if self.has_option(section, option) else default)
+ return (
+ RawConfigParser.get(self, section, option, **kwargs)
+ if self.has_option(section, option)
+ else default
+ )
def getint(self, section, option, default=None):
"""
@@ -55,7 +58,7 @@ def getboolean(self, section, option, default=False):
"""
val = self.get(section, option)
- return val.lower() in ('1', 'on', 'true', 'yes') if val else default
+ return val.lower() in ("1", "on", "true", "yes") if val else default
def getlist(self, section, option, default=None):
"""
@@ -88,9 +91,8 @@ def getlist(self, section, option, default=None):
value = self.get(section, option)
- if '\n' in value:
- return [item.strip()
- for item in value.splitlines() if item.strip()]
+ if "\n" in value:
+ return [item.strip() for item in value.splitlines() if item.strip()]
else:
return shlex.split(value)
diff --git a/watson/frames.py b/watson/frames.py
index 0e511578..600182c2 100644
--- a/watson/frames.py
+++ b/watson/frames.py
@@ -4,11 +4,19 @@
from collections import namedtuple
-HEADERS = ('start', 'stop', 'project', 'id', 'tags', 'updated_at')
+HEADERS = ("start", "stop", "project", "id", "tags", "updated_at")
-class Frame(namedtuple('Frame', HEADERS)):
- def __new__(cls, start, stop, project, id, tags=None, updated_at=None,):
+class Frame(namedtuple("Frame", HEADERS)):
+ def __new__(
+ cls,
+ start,
+ stop,
+ project,
+ id,
+ tags=None,
+ updated_at=None,
+ ):
try:
if not isinstance(start, arrow.Arrow):
start = arrow.get(start)
@@ -22,12 +30,13 @@ def __new__(cls, start, stop, project, id, tags=None, updated_at=None,):
updated_at = arrow.get(updated_at)
except (ValueError, TypeError) as e:
from .watson import WatsonError
+
raise WatsonError("Error converting date: {}".format(e))
- start = start.to('local')
+ start = start.to("local")
if stop:
- stop = stop.to('local')
+ stop = stop.to("local")
if tags is None:
tags = []
@@ -37,15 +46,15 @@ def __new__(cls, start, stop, project, id, tags=None, updated_at=None,):
)
def dump(self):
- start = self.start.to('utc').int_timestamp
- stop = self.stop.to('utc').int_timestamp if self.stop else None
+ start = self.start.to("utc").int_timestamp
+ stop = self.stop.to("utc").int_timestamp if self.stop else None
updated_at = self.updated_at.int_timestamp
return (start, stop, self.project, self.id, self.tags, updated_at)
@property
def day(self):
- return self.start.floor('day')
+ return self.start.floor("day")
def __lt__(self, other):
return self.start < other.start
@@ -61,7 +70,7 @@ def __gte__(self, other):
class Span(object):
- def __init__(self, start, stop, timeframe='day'):
+ def __init__(self, start, stop, timeframe="day"):
self.timeframe = timeframe
self.start = start.floor(self.timeframe)
self.stop = stop.ceil(self.timeframe)
@@ -121,9 +130,7 @@ def __delitem__(self, key):
def _get_index_by_id(self, id):
try:
- return next(
- i for i, v in enumerate(self['id']) if v.startswith(id)
- )
+ return next(i for i, v in enumerate(self["id"]) if v.startswith(id))
except StopIteration:
raise KeyError("Frame with id {} not found.".format(id))
@@ -138,12 +145,10 @@ def add(self, *args, **kwargs):
self._rows.append(frame)
return frame
- def new_frame(self, project, start, stop, tags=None, id=None,
- updated_at=None):
+ def new_frame(self, project, start, stop, tags=None, id=None, updated_at=None):
if not id:
id = uuid.uuid4().hex
- return Frame(start, stop, project, id, tags=tags,
- updated_at=updated_at)
+ return Frame(start, stop, project, id, tags=tags, updated_at=updated_at)
def dump(self):
return tuple(frame.dump() for frame in self._rows)
@@ -161,14 +166,14 @@ def filter(
for frame in self._rows:
if projects is not None and frame.project not in projects:
continue
- if ignore_projects is not None and\
- frame.project in ignore_projects:
+ if ignore_projects is not None and frame.project in ignore_projects:
continue
if tags is not None and not any(tag in frame.tags for tag in tags):
continue
- if ignore_tags is not None and\
- any(tag in frame.tags for tag in ignore_tags):
+ if ignore_tags is not None and any(
+ tag in frame.tags for tag in ignore_tags
+ ):
continue
if span is None:
diff --git a/watson/fullmoon.py b/watson/fullmoon.py
index 5abd5535..5b1c5953 100644
--- a/watson/fullmoon.py
+++ b/watson/fullmoon.py
@@ -5,213 +5,1243 @@
# data source http://home.hiwaay.net/~krcool/Astro/moon/fullmoon.htm
fullmoons = [
- 948429720, 950977680, 953527560, 956079780, 958635300, 961194480,
- 963755820, 966316440, 968873880, 971427240, 973977360, 976525500,
- 979071960, 981616380, 984158640, 986700240, 989243700, 991791660,
- 994345560, 996904620, 999467040, 1002030600, 1004593320, 1007153400,
- 1009708920, 1012258320, 1014801480, 1017340020, 1019876580, 1022414040,
- 1024955100, 1027501680, 1030055400, 1032616800, 1035184860, 1037756100,
- 1040325060, 1042886940, 1045439520, 1047983760, 1050521880, 1053056280,
- 1055589480, 1058124180, 1060663740, 1063211820, 1065770940, 1068340500,
- 1070915880, 1073490060, 1076057280, 1078614960, 1081163040, 1083702900,
- 1086236460, 1088766600, 1091297160, 1093832580, 1096377000, 1098932940,
- 1101499740, 1104073680, 1106649180, 1109220900, 1111784400, 1114337280,
- 1116879600, 1119413700, 1121943660, 1124474040, 1127008860, 1129551300,
- 1132102740, 1134663420, 1137232140, 1139805900, 1142379420, 1144946520,
- 1147503180, 1150049040, 1152586980, 1155120840, 1157654520, 1160190780,
- 1162731540, 1165278360, 1167832740, 1170395220, 1172963880, 1175534160,
- 1178100660, 1180659900, 1183211400, 1185756480, 1188297300, 1190835960,
- 1193374380, 1195914660, 1198459020, 1201008960, 1203564720, 1206124860,
- 1208687220, 1211249580, 1213810320, 1216368000, 1218921480, 1221470100,
- 1224014640, 1226557140, 1229099940, 1231644480, 1234191060, 1236739200,
- 1239289080, 1241841780, 1244398380, 1246958580, 1249520220, 1252080300,
- 1254636660, 1257189300, 1259739180, 1262286900, 1264832340, 1267375200,
- 1269916080, 1272457260, 1275001740, 1277551920, 1280108280, 1282669560,
- 1285233540, 1287797880, 1290360480, 1292919300, 1295472180, 1298018280,
- 1300558320, 1303094760, 1305630600, 1308168900, 1310712060, 1313261940,
- 1315819680, 1318385280, 1320956280, 1323527820, 1326094260, 1328651760,
- 1331199720, 1333740060, 1336275420, 1338808380, 1341341640, 1343878140,
- 1346421600, 1348975260, 1351540320, 1354114080, 1356690120, 1359261600,
- 1361824080, 1364376540, 1366919940, 1369456020, 1371987240, 1374517080,
- 1377049620, 1379589300, 1382139600, 1384701480, 1387272600, 1389848040,
- 1392422100, 1394989800, 1397547840, 1400095020, 1402632780, 1405164360,
- 1407694260, 1410226800, 1412765520, 1415312640, 1417868940, 1420433700,
- 1423005060, 1425578820, 1428149220, 1430710980, 1433262000, 1435803660,
- 1438339440, 1440873420, 1443408720, 1445947560, 1448491500, 1451041980,
- 1453600080, 1456165320, 1458734520, 1461302700, 1463865360, 1466420640,
- 1468969140, 1471512540, 1474052820, 1476591900, 1479131580, 1481674020,
- 1484220960, 1486773240, 1489330500, 1491890940, 1494452640, 1497013860,
- 1499573340, 1502129580, 1504681440, 1507228860, 1509773040, 1512316140,
- 1514859960, 1517405280, 1519951920, 1522499880, 1525050000, 1527603660,
- 1530161700, 1532722920, 1535284680, 1537844040, 1540399560, 1542951660,
- 1545501000, 1548047820, 1550591640, 1553132640, 1555672440, 1558213980,
- 1560760320, 1563313200, 1565872260, 1568435640, 1571000940, 1573565760,
- 1576127640, 1578684180, 1581233700, 1583776140, 1586313420, 1588848420,
- 1591384500, 1593924360, 1596470400, 1599024180, 1601586360, 1604155860,
- 1606728660, 1609298940, 1611861420, 1614413940, 1616957400, 1619494440,
- 1622027760, 1624560120, 1627094340, 1629633780, 1632182160, 1634741880,
- 1637312340, 1639888620, 1642463400, 1645030680, 1647587940, 1650135420,
- 1652674560, 1655207580, 1657737540, 1660268160, 1662803940, 1665348960,
- 1667905440, 1670472600, 1673046600, 1675621800, 1678192920, 1680755760,
- 1683308160, 1685850180, 1688384340, 1690914720, 1693445760, 1695981480,
- 1698524700, 1701076680, 1703637300, 1706205360, 1708777920, 1711350120,
- 1713916260, 1716472500, 1719018540, 1721557080, 1724091960, 1726626900,
- 1729164480, 1731706200, 1734253380, 1736807340, 1739368500, 1741935360,
- 1744503840, 1747069080, 1749627960, 1752179880, 1754726160, 1757268540,
- 1759808880, 1762348860, 1764890160, 1767434640, 1769983860, 1772538000,
- 1775096040, 1777656300, 1780217220, 1782777480, 1785335820, 1787890800,
- 1790441400, 1792987980, 1795532100, 1798075800, 1800620340, 1803165960,
- 1805712420, 1808260200, 1810810860, 1813365960, 1815925620, 1818487860,
- 1821049560, 1823608140, 1826162880, 1828714260, 1831262700, 1833807960,
- 1836349740, 1838889000, 1841428260, 1843971060, 1846519980, 1849075920,
- 1851637800, 1854203220, 1856769540, 1859334120, 1861894200, 1864447500,
- 1866993180, 1869532140, 1872067140, 1874601600, 1877138700, 1879681080,
- 1882230780, 1884789060, 1887355800, 1889928240, 1892501220, 1895068500,
- 1897626120, 1900173540, 1902712920, 1905247260, 1907779380, 1910312040,
- 1912848360, 1915392000, 1917946140, 1920511980, 1923086520, 1925663220,
- 1928234880, 1930797060, 1933348980, 1935891660, 1938427200, 1940958180,
- 1943488020, 1946020920, 1948561200, 1951112100, 1953674460, 1956245700,
- 1958820840, 1961394300, 1963961280, 1966518660, 1969065480, 1971603240,
- 1974135180, 1976665740, 1979199120, 1981738800, 1984286640, 1986843060,
- 1989407340, 1991977560, 1994549940, 1997119140, 1999680240, 2002231260,
- 2004773400, 2007310140, 2009845320, 2012382000, 2014922040, 2017466640,
- 2020016940, 2022574020, 2025137520, 2027704800, 2030271420, 2032833300,
- 2035388760, 2037938160, 2040483060, 2043025080, 2045565840, 2048106780,
- 2050649760, 2053196280, 2055747300, 2058302640, 2060860920, 2063420820,
- 2065981140, 2068540740, 2071098120, 2073651900, 2076201420, 2078747400,
- 2081291700, 2083835880, 2086380600, 2088925860, 2091471840, 2094019860,
- 2096571780, 2099128860, 2101690260, 2104253220, 2106814560, 2109372300,
- 2111926200, 2114476680, 2117023560, 2119566600, 2122106100, 2124644160,
- 2127184020, 2129728920, 2132281020, 2134840260, 2137404780, 2139971880,
- 2142538620, 2145102000, 2147659320, 2150208660, 2152750260, 2155286280,
- 2157819960, 2160354780, 2162893800, 2165439480, 2167993500, 2170556580,
- 2173127340, 2175701520, 2178272820, 2180835660, 2183387820, 2185930500,
- 2188466520, 2190999000, 2193530700, 2196064680, 2198604240, 2201153040,
- 2203713480, 2206284720, 2208861540, 2211436560, 2214003660, 2216560380,
- 2219107200, 2221645740, 2224178460, 2226708420, 2229239460, 2231775840,
- 2234321460, 2236878480, 2239445880, 2242019580, 2244594120, 2247164460,
- 2249726580, 2252278440, 2254820400, 2257354920, 2259885900, 2262417840,
- 2264954580, 2267498700, 2270051040, 2272611300, 2275178340, 2277749460,
- 2280320280, 2282885460, 2285441400, 2287987860, 2290527240, 2293063320,
- 2295599640, 2298138540, 2300681220, 2303228640, 2305781880, 2308341540,
- 2310906420, 2313473040, 2316037140, 2318595720, 2321148360, 2323695900,
- 2326240080, 2328782220, 2331323640, 2333865840, 2336410380, 2338958580,
- 2341510980, 2344066860, 2346625080, 2349184680, 2351744640, 2354303760,
- 2356860360, 2359413120, 2361961740, 2364507360, 2367051720, 2369596020,
- 2372140500, 2374685160, 2377230900, 2379779700, 2382333480, 2384892780,
- 2387455800, 2390019240, 2392579980, 2395136700, 2397689460, 2400238380,
- 2402783220, 2405323800, 2407861440, 2410399080, 2412940320, 2415488220,
- 2418043920, 2420606520, 2423173440, 2425741560, 2428307820, 2430868980,
- 2433422520, 2435967600, 2438505480, 2441039220, 2443572420, 2446108560,
- 2448650400, 2451200160, 2453759040, 2456326800, 2458900620, 2461474680,
- 2464042560, 2466600000, 2469146820, 2471685300, 2474218740, 2476750200,
- 2479282560, 2481818940, 2484362940, 2486917680, 2489484120, 2492059260,
- 2494636200, 2497207740, 2499769500, 2502320820, 2504862960, 2507398080,
- 2509929060, 2512459260, 2514992760, 2517533700, 2520085200, 2522647800,
- 2525218860, 2527793340, 2530365900, 2532932040, 2535488880, 2538035580,
- 2540573520, 2543106120, 2545637520, 2548171980, 2550712620, 2553261060,
- 2555817420, 2558380920, 2560949700, 2563520520, 2566088460, 2568648960,
- 2571200160, 2573743080, 2576281020, 2578817580, 2581355700, 2583896880,
- 2586442020, 2588991960, 2591547780, 2594109420, 2596674660, 2599239660,
- 2601800940, 2604356700, 2606907300, 2609453820, 2611997820, 2614540260,
- 2617082340, 2619625680, 2622171540, 2624721060, 2627274240, 2629830420,
- 2632388640, 2634948180, 2637508140, 2640066900, 2642622720, 2645174400,
- 2647722180, 2650267500, 2652811800, 2655355680, 2657899380, 2660443440,
- 2662989480, 2665539840, 2668096140, 2670657840, 2673222180, 2675785560,
- 2678345400, 2680900980, 2683452240, 2685999000, 2688541080, 2691079200,
- 2693615640, 2696153880, 2698697640, 2701249140, 2703808680, 2706374400,
- 2708943180, 2711511720, 2714076420, 2716634220, 2719183320, 2721724020,
- 2724258780, 2726791260, 2729325000, 2731863420, 2734408920, 2736963360,
- 2739527280, 2742099240, 2744674500, 2747246520, 2749809480, 2752361220,
- 2754903060, 2757438300, 2759970060, 2762501400, 2765035380, 2767575240,
- 2770124580, 2772685560, 2775257340, 2777834400, 2780409360, 2782976100,
- 2785532280, 2788078500, 2790616620, 2793149280, 2795679540, 2798211120,
- 2800748220, 2803294560, 2805851940, 2808419280, 2810992380, 2813566080,
- 2816135400, 2818696740, 2821248360, 2823790440, 2826325500, 2828857320,
- 2831390340, 2833928160, 2836473060, 2839025640, 2841585360, 2844151080,
- 2846720580, 2849289840, 2851854120, 2854409940, 2856957000, 2859497520,
- 2862035040, 2864572920, 2867113140, 2869656600, 2872203960, 2874756300,
- 2877314220, 2879876940, 2882441700, 2885004720, 2887563300, 2890116780,
- 2892665940, 2895212040, 2897756040, 2900298900, 2902841700, 2905385940,
- 2907933000, 2910483480, 2913037200, 2915593500, 2918151960, 2920711740,
- 2923271880, 2925830340, 2928385200, 2930935800, 2933482800, 2936027640,
- 2938571460, 2941114680, 2943657480, 2946201240, 2948748300, 2951301000,
- 2953860120, 2956424160, 2958989400, 2961552240, 2964110880, 2966664840,
- 2969213940, 2971758120, 2974297440, 2976833520, 2979369540, 2981909520,
- 2984456580, 2987012280, 2989575660, 2992144080, 2994714000, 2997281760,
- 2999843700, 3002397240, 3004941540, 3007478340, 3010010880, 3012543060,
- 3015078480, 3017620080, 3020170020, 3022729620, 3025298400, 3027873240,
- 3030448140, 3033016260, 3035573460, 3038119560, 3040657260, 3043189980,
- 3045721020, 3048253260, 3050789940, 3053334420, 3055889700, 3058456740,
- 3061032180, 3063609060, 3066180240, 3068741460, 3071292120, 3073833840,
- 3076368840, 3078900000, 3081430740, 3083964960, 3086506680, 3089058780,
- 3091621440, 3094192020, 3096765600, 3099337080, 3101902260, 3104458560,
- 3107005260, 3109543740, 3112077180, 3114609660, 3117145320, 3119686980,
- 3122235840, 3124791960, 3127354320, 3129921480, 3132490500, 3135057180,
- 3137617260, 3140168880, 3142712820, 3145252140, 3147790320, 3150329880,
- 3152872080, 3155417520, 3157966920, 3160521240, 3163080780, 3165643980,
- 3168207540, 3170768340, 3173324700, 3175876560, 3178425000, 3180970860,
- 3183514980, 3186058080, 3188601480, 3191146560, 3193694400, 3196245480,
- 3198799560, 3201356280, 3203915280, 3206475720, 3209036040, 3211593960,
- 3214147740, 3216697200, 3219243360, 3221787480, 3224330340, 3226872420,
- 3229414560, 3231958860, 3234507840, 3237063540, 3239625720, 3242191560,
- 3244756980, 3247318860, 3249875880, 3252427680, 3254974140, 3257515140,
- 3260051880, 3262586760, 3265123680, 3267666480, 3270217680, 3272777640,
- 3275344560, 3277915020, 3280485060, 3283050780, 3285608820, 3288157440,
- 3290697180, 3293230860, 3295762260, 3298295280, 3300833280, 3303378900,
- 3305933820, 3308498580, 3311071500, 3313647660, 3316220040, 3318782880,
- 3321334020, 3323875200, 3326409660, 3328940940, 3331472220, 3334006380,
- 3336546780, 3339096660, 3341658180, 3344230200, 3346807320, 3349381860,
- 3351947940, 3354503520, 3357049260, 3359587200, 3362120040, 3364650840,
- 3367183200, 3369721140, 3372268200, 3374825880, 3377392800, 3379965000,
- 3382537500, 3385105680, 3387666360, 3390217740, 3392760240, 3395296080,
- 3397829040, 3400363320, 3402902280, 3405447900, 3408000420, 3410559360,
- 3413123520, 3415691160, 3418258860, 3420822300, 3423378240, 3425926140,
- 3428467980, 3431007120, 3433546620, 3436088100, 3438632220, 3441179340,
- 3443730480, 3446286480, 3448847100, 3451410060, 3453972060, 3456530760,
- 3459085320, 3461636280, 3464184360, 3466730220, 3469274340, 3471817620,
- 3474361440, 3476907120, 3479455620, 3482007180, 3484561680, 3487119060,
- 3489678960, 3492240240, 3494800620, 3497357700, 3499910220, 3502458420,
- 3505003440, 3507546600, 3510088380, 3512629440, 3515171340, 3517716900,
- 3520268700, 3522827880, 3525392940, 3527959980, 3530524920, 3533085240,
- 3535640040, 3538189200, 3540732540, 3543270600, 3545805180, 3548339880,
- 3550878780, 3553425300, 3555981120, 3558545400, 3561115260, 3563686740,
- 3566255700, 3568818180, 3571371480, 3573915060, 3576450780, 3578982300,
- 3581513640, 3584048580, 3586590120, 3589140480, 3591700740, 3594270360,
- 3596846040, 3599421420, 3601989540, 3604546320, 3607091760, 3609628800,
- 3612160980, 3614691840, 3617224320, 3619761480, 3622306500, 3624862380,
- 3627429720, 3630005100, 3632581620, 3635152140, 3637712640, 3640262820,
- 3642804300, 3645339420, 3647871120, 3650402580, 3652937760, 3655480260,
- 3658032780, 3660595320, 3663165060, 3665737440, 3668307660, 3670871880,
- 3673427820, 3675974760, 3678513960, 3681048420, 3683582220, 3686119140,
- 3688661700, 3691210920, 3693766440, 3696327480, 3698892720, 3701460000,
- 3704025480, 3706585260, 3709137420, 3711682560, 3714223500, 3716763420,
- 3719304480, 3721847640, 3724393200, 3726941760, 3729494400, 3732051780,
- 3734612880, 3737175060, 3739735500, 3742292580, 3744846060, 3747396480,
- 3749944380, 3752490060, 3755034000, 3757577280, 3760121280, 3762667380,
- 3765216300, 3767768400, 3770323800, 3772882380, 3775443540, 3778005480,
- 3780565560, 3783121500, 3785672400, 3788219160, 3790762920, 3793304640,
- 3795845100, 3798385440, 3800928060, 3803475960, 3806031240, 3808594020,
- 3811161360, 3813728820, 3816292560, 3818850780, 3821402940, 3823948860,
- 3826488840, 3829024140, 3831557640, 3834093420, 3836635500, 3839186520,
- 3841747140, 3844315200, 3846887160, 3849458520, 3852025020, 3854583120,
- 3857131080, 3859669920, 3862202580, 3864733140, 3867265620, 3869803500,
- 3872349360, 3874904880, 3877470420, 3880044120, 3882620760, 3885193260,
- 3887755740, 3890306340, 3892846800, 3895380780, 3897911820, 3900443220,
- 3902977800, 3905518800, 3908069280, 3910631220, 3913203240, 3915779940,
- 3918353820, 3920919240, 3923474220, 3926019660, 3928557660, 3931090920,
- 3933622440, 3936155700, 3938694600, 3941242260, 3943799940, 3946366260,
- 3948937320, 3951508380, 3954075420, 3956635440, 3959186880, 3961729980,
- 3964266900, 3966801120, 3969336720, 3971876880, 3974423040, 3976975380,
- 3979533180, 3982095600, 3984661260, 3987227400, 3989790120, 3992346300,
- 3994895280, 3997438680, 3999979560, 4002520740, 4005063420, 4007607960,
- 4010154600, 4012704300, 4015258320, 4017816720, 4020377940, 4022939220,
- 4025498220, 4028054100, 4030606920, 4033157100, 4035704760, 4038250020,
- 4040793540, 4043336640, 4045880820, 4048427280, 4050976800, 4053529680,
- 4056086220, 4058646360, 4061208960, 4063771380, 4066330620, 4068884940,
- 4071434040, 4073979120, 4076521380, 4079061660, 4081600980, 4084141260,
- 4086685440, 4089236520, 4091795880, 4094362140, 4096931040, 4099497900,
- 4102059660
+ 948429720,
+ 950977680,
+ 953527560,
+ 956079780,
+ 958635300,
+ 961194480,
+ 963755820,
+ 966316440,
+ 968873880,
+ 971427240,
+ 973977360,
+ 976525500,
+ 979071960,
+ 981616380,
+ 984158640,
+ 986700240,
+ 989243700,
+ 991791660,
+ 994345560,
+ 996904620,
+ 999467040,
+ 1002030600,
+ 1004593320,
+ 1007153400,
+ 1009708920,
+ 1012258320,
+ 1014801480,
+ 1017340020,
+ 1019876580,
+ 1022414040,
+ 1024955100,
+ 1027501680,
+ 1030055400,
+ 1032616800,
+ 1035184860,
+ 1037756100,
+ 1040325060,
+ 1042886940,
+ 1045439520,
+ 1047983760,
+ 1050521880,
+ 1053056280,
+ 1055589480,
+ 1058124180,
+ 1060663740,
+ 1063211820,
+ 1065770940,
+ 1068340500,
+ 1070915880,
+ 1073490060,
+ 1076057280,
+ 1078614960,
+ 1081163040,
+ 1083702900,
+ 1086236460,
+ 1088766600,
+ 1091297160,
+ 1093832580,
+ 1096377000,
+ 1098932940,
+ 1101499740,
+ 1104073680,
+ 1106649180,
+ 1109220900,
+ 1111784400,
+ 1114337280,
+ 1116879600,
+ 1119413700,
+ 1121943660,
+ 1124474040,
+ 1127008860,
+ 1129551300,
+ 1132102740,
+ 1134663420,
+ 1137232140,
+ 1139805900,
+ 1142379420,
+ 1144946520,
+ 1147503180,
+ 1150049040,
+ 1152586980,
+ 1155120840,
+ 1157654520,
+ 1160190780,
+ 1162731540,
+ 1165278360,
+ 1167832740,
+ 1170395220,
+ 1172963880,
+ 1175534160,
+ 1178100660,
+ 1180659900,
+ 1183211400,
+ 1185756480,
+ 1188297300,
+ 1190835960,
+ 1193374380,
+ 1195914660,
+ 1198459020,
+ 1201008960,
+ 1203564720,
+ 1206124860,
+ 1208687220,
+ 1211249580,
+ 1213810320,
+ 1216368000,
+ 1218921480,
+ 1221470100,
+ 1224014640,
+ 1226557140,
+ 1229099940,
+ 1231644480,
+ 1234191060,
+ 1236739200,
+ 1239289080,
+ 1241841780,
+ 1244398380,
+ 1246958580,
+ 1249520220,
+ 1252080300,
+ 1254636660,
+ 1257189300,
+ 1259739180,
+ 1262286900,
+ 1264832340,
+ 1267375200,
+ 1269916080,
+ 1272457260,
+ 1275001740,
+ 1277551920,
+ 1280108280,
+ 1282669560,
+ 1285233540,
+ 1287797880,
+ 1290360480,
+ 1292919300,
+ 1295472180,
+ 1298018280,
+ 1300558320,
+ 1303094760,
+ 1305630600,
+ 1308168900,
+ 1310712060,
+ 1313261940,
+ 1315819680,
+ 1318385280,
+ 1320956280,
+ 1323527820,
+ 1326094260,
+ 1328651760,
+ 1331199720,
+ 1333740060,
+ 1336275420,
+ 1338808380,
+ 1341341640,
+ 1343878140,
+ 1346421600,
+ 1348975260,
+ 1351540320,
+ 1354114080,
+ 1356690120,
+ 1359261600,
+ 1361824080,
+ 1364376540,
+ 1366919940,
+ 1369456020,
+ 1371987240,
+ 1374517080,
+ 1377049620,
+ 1379589300,
+ 1382139600,
+ 1384701480,
+ 1387272600,
+ 1389848040,
+ 1392422100,
+ 1394989800,
+ 1397547840,
+ 1400095020,
+ 1402632780,
+ 1405164360,
+ 1407694260,
+ 1410226800,
+ 1412765520,
+ 1415312640,
+ 1417868940,
+ 1420433700,
+ 1423005060,
+ 1425578820,
+ 1428149220,
+ 1430710980,
+ 1433262000,
+ 1435803660,
+ 1438339440,
+ 1440873420,
+ 1443408720,
+ 1445947560,
+ 1448491500,
+ 1451041980,
+ 1453600080,
+ 1456165320,
+ 1458734520,
+ 1461302700,
+ 1463865360,
+ 1466420640,
+ 1468969140,
+ 1471512540,
+ 1474052820,
+ 1476591900,
+ 1479131580,
+ 1481674020,
+ 1484220960,
+ 1486773240,
+ 1489330500,
+ 1491890940,
+ 1494452640,
+ 1497013860,
+ 1499573340,
+ 1502129580,
+ 1504681440,
+ 1507228860,
+ 1509773040,
+ 1512316140,
+ 1514859960,
+ 1517405280,
+ 1519951920,
+ 1522499880,
+ 1525050000,
+ 1527603660,
+ 1530161700,
+ 1532722920,
+ 1535284680,
+ 1537844040,
+ 1540399560,
+ 1542951660,
+ 1545501000,
+ 1548047820,
+ 1550591640,
+ 1553132640,
+ 1555672440,
+ 1558213980,
+ 1560760320,
+ 1563313200,
+ 1565872260,
+ 1568435640,
+ 1571000940,
+ 1573565760,
+ 1576127640,
+ 1578684180,
+ 1581233700,
+ 1583776140,
+ 1586313420,
+ 1588848420,
+ 1591384500,
+ 1593924360,
+ 1596470400,
+ 1599024180,
+ 1601586360,
+ 1604155860,
+ 1606728660,
+ 1609298940,
+ 1611861420,
+ 1614413940,
+ 1616957400,
+ 1619494440,
+ 1622027760,
+ 1624560120,
+ 1627094340,
+ 1629633780,
+ 1632182160,
+ 1634741880,
+ 1637312340,
+ 1639888620,
+ 1642463400,
+ 1645030680,
+ 1647587940,
+ 1650135420,
+ 1652674560,
+ 1655207580,
+ 1657737540,
+ 1660268160,
+ 1662803940,
+ 1665348960,
+ 1667905440,
+ 1670472600,
+ 1673046600,
+ 1675621800,
+ 1678192920,
+ 1680755760,
+ 1683308160,
+ 1685850180,
+ 1688384340,
+ 1690914720,
+ 1693445760,
+ 1695981480,
+ 1698524700,
+ 1701076680,
+ 1703637300,
+ 1706205360,
+ 1708777920,
+ 1711350120,
+ 1713916260,
+ 1716472500,
+ 1719018540,
+ 1721557080,
+ 1724091960,
+ 1726626900,
+ 1729164480,
+ 1731706200,
+ 1734253380,
+ 1736807340,
+ 1739368500,
+ 1741935360,
+ 1744503840,
+ 1747069080,
+ 1749627960,
+ 1752179880,
+ 1754726160,
+ 1757268540,
+ 1759808880,
+ 1762348860,
+ 1764890160,
+ 1767434640,
+ 1769983860,
+ 1772538000,
+ 1775096040,
+ 1777656300,
+ 1780217220,
+ 1782777480,
+ 1785335820,
+ 1787890800,
+ 1790441400,
+ 1792987980,
+ 1795532100,
+ 1798075800,
+ 1800620340,
+ 1803165960,
+ 1805712420,
+ 1808260200,
+ 1810810860,
+ 1813365960,
+ 1815925620,
+ 1818487860,
+ 1821049560,
+ 1823608140,
+ 1826162880,
+ 1828714260,
+ 1831262700,
+ 1833807960,
+ 1836349740,
+ 1838889000,
+ 1841428260,
+ 1843971060,
+ 1846519980,
+ 1849075920,
+ 1851637800,
+ 1854203220,
+ 1856769540,
+ 1859334120,
+ 1861894200,
+ 1864447500,
+ 1866993180,
+ 1869532140,
+ 1872067140,
+ 1874601600,
+ 1877138700,
+ 1879681080,
+ 1882230780,
+ 1884789060,
+ 1887355800,
+ 1889928240,
+ 1892501220,
+ 1895068500,
+ 1897626120,
+ 1900173540,
+ 1902712920,
+ 1905247260,
+ 1907779380,
+ 1910312040,
+ 1912848360,
+ 1915392000,
+ 1917946140,
+ 1920511980,
+ 1923086520,
+ 1925663220,
+ 1928234880,
+ 1930797060,
+ 1933348980,
+ 1935891660,
+ 1938427200,
+ 1940958180,
+ 1943488020,
+ 1946020920,
+ 1948561200,
+ 1951112100,
+ 1953674460,
+ 1956245700,
+ 1958820840,
+ 1961394300,
+ 1963961280,
+ 1966518660,
+ 1969065480,
+ 1971603240,
+ 1974135180,
+ 1976665740,
+ 1979199120,
+ 1981738800,
+ 1984286640,
+ 1986843060,
+ 1989407340,
+ 1991977560,
+ 1994549940,
+ 1997119140,
+ 1999680240,
+ 2002231260,
+ 2004773400,
+ 2007310140,
+ 2009845320,
+ 2012382000,
+ 2014922040,
+ 2017466640,
+ 2020016940,
+ 2022574020,
+ 2025137520,
+ 2027704800,
+ 2030271420,
+ 2032833300,
+ 2035388760,
+ 2037938160,
+ 2040483060,
+ 2043025080,
+ 2045565840,
+ 2048106780,
+ 2050649760,
+ 2053196280,
+ 2055747300,
+ 2058302640,
+ 2060860920,
+ 2063420820,
+ 2065981140,
+ 2068540740,
+ 2071098120,
+ 2073651900,
+ 2076201420,
+ 2078747400,
+ 2081291700,
+ 2083835880,
+ 2086380600,
+ 2088925860,
+ 2091471840,
+ 2094019860,
+ 2096571780,
+ 2099128860,
+ 2101690260,
+ 2104253220,
+ 2106814560,
+ 2109372300,
+ 2111926200,
+ 2114476680,
+ 2117023560,
+ 2119566600,
+ 2122106100,
+ 2124644160,
+ 2127184020,
+ 2129728920,
+ 2132281020,
+ 2134840260,
+ 2137404780,
+ 2139971880,
+ 2142538620,
+ 2145102000,
+ 2147659320,
+ 2150208660,
+ 2152750260,
+ 2155286280,
+ 2157819960,
+ 2160354780,
+ 2162893800,
+ 2165439480,
+ 2167993500,
+ 2170556580,
+ 2173127340,
+ 2175701520,
+ 2178272820,
+ 2180835660,
+ 2183387820,
+ 2185930500,
+ 2188466520,
+ 2190999000,
+ 2193530700,
+ 2196064680,
+ 2198604240,
+ 2201153040,
+ 2203713480,
+ 2206284720,
+ 2208861540,
+ 2211436560,
+ 2214003660,
+ 2216560380,
+ 2219107200,
+ 2221645740,
+ 2224178460,
+ 2226708420,
+ 2229239460,
+ 2231775840,
+ 2234321460,
+ 2236878480,
+ 2239445880,
+ 2242019580,
+ 2244594120,
+ 2247164460,
+ 2249726580,
+ 2252278440,
+ 2254820400,
+ 2257354920,
+ 2259885900,
+ 2262417840,
+ 2264954580,
+ 2267498700,
+ 2270051040,
+ 2272611300,
+ 2275178340,
+ 2277749460,
+ 2280320280,
+ 2282885460,
+ 2285441400,
+ 2287987860,
+ 2290527240,
+ 2293063320,
+ 2295599640,
+ 2298138540,
+ 2300681220,
+ 2303228640,
+ 2305781880,
+ 2308341540,
+ 2310906420,
+ 2313473040,
+ 2316037140,
+ 2318595720,
+ 2321148360,
+ 2323695900,
+ 2326240080,
+ 2328782220,
+ 2331323640,
+ 2333865840,
+ 2336410380,
+ 2338958580,
+ 2341510980,
+ 2344066860,
+ 2346625080,
+ 2349184680,
+ 2351744640,
+ 2354303760,
+ 2356860360,
+ 2359413120,
+ 2361961740,
+ 2364507360,
+ 2367051720,
+ 2369596020,
+ 2372140500,
+ 2374685160,
+ 2377230900,
+ 2379779700,
+ 2382333480,
+ 2384892780,
+ 2387455800,
+ 2390019240,
+ 2392579980,
+ 2395136700,
+ 2397689460,
+ 2400238380,
+ 2402783220,
+ 2405323800,
+ 2407861440,
+ 2410399080,
+ 2412940320,
+ 2415488220,
+ 2418043920,
+ 2420606520,
+ 2423173440,
+ 2425741560,
+ 2428307820,
+ 2430868980,
+ 2433422520,
+ 2435967600,
+ 2438505480,
+ 2441039220,
+ 2443572420,
+ 2446108560,
+ 2448650400,
+ 2451200160,
+ 2453759040,
+ 2456326800,
+ 2458900620,
+ 2461474680,
+ 2464042560,
+ 2466600000,
+ 2469146820,
+ 2471685300,
+ 2474218740,
+ 2476750200,
+ 2479282560,
+ 2481818940,
+ 2484362940,
+ 2486917680,
+ 2489484120,
+ 2492059260,
+ 2494636200,
+ 2497207740,
+ 2499769500,
+ 2502320820,
+ 2504862960,
+ 2507398080,
+ 2509929060,
+ 2512459260,
+ 2514992760,
+ 2517533700,
+ 2520085200,
+ 2522647800,
+ 2525218860,
+ 2527793340,
+ 2530365900,
+ 2532932040,
+ 2535488880,
+ 2538035580,
+ 2540573520,
+ 2543106120,
+ 2545637520,
+ 2548171980,
+ 2550712620,
+ 2553261060,
+ 2555817420,
+ 2558380920,
+ 2560949700,
+ 2563520520,
+ 2566088460,
+ 2568648960,
+ 2571200160,
+ 2573743080,
+ 2576281020,
+ 2578817580,
+ 2581355700,
+ 2583896880,
+ 2586442020,
+ 2588991960,
+ 2591547780,
+ 2594109420,
+ 2596674660,
+ 2599239660,
+ 2601800940,
+ 2604356700,
+ 2606907300,
+ 2609453820,
+ 2611997820,
+ 2614540260,
+ 2617082340,
+ 2619625680,
+ 2622171540,
+ 2624721060,
+ 2627274240,
+ 2629830420,
+ 2632388640,
+ 2634948180,
+ 2637508140,
+ 2640066900,
+ 2642622720,
+ 2645174400,
+ 2647722180,
+ 2650267500,
+ 2652811800,
+ 2655355680,
+ 2657899380,
+ 2660443440,
+ 2662989480,
+ 2665539840,
+ 2668096140,
+ 2670657840,
+ 2673222180,
+ 2675785560,
+ 2678345400,
+ 2680900980,
+ 2683452240,
+ 2685999000,
+ 2688541080,
+ 2691079200,
+ 2693615640,
+ 2696153880,
+ 2698697640,
+ 2701249140,
+ 2703808680,
+ 2706374400,
+ 2708943180,
+ 2711511720,
+ 2714076420,
+ 2716634220,
+ 2719183320,
+ 2721724020,
+ 2724258780,
+ 2726791260,
+ 2729325000,
+ 2731863420,
+ 2734408920,
+ 2736963360,
+ 2739527280,
+ 2742099240,
+ 2744674500,
+ 2747246520,
+ 2749809480,
+ 2752361220,
+ 2754903060,
+ 2757438300,
+ 2759970060,
+ 2762501400,
+ 2765035380,
+ 2767575240,
+ 2770124580,
+ 2772685560,
+ 2775257340,
+ 2777834400,
+ 2780409360,
+ 2782976100,
+ 2785532280,
+ 2788078500,
+ 2790616620,
+ 2793149280,
+ 2795679540,
+ 2798211120,
+ 2800748220,
+ 2803294560,
+ 2805851940,
+ 2808419280,
+ 2810992380,
+ 2813566080,
+ 2816135400,
+ 2818696740,
+ 2821248360,
+ 2823790440,
+ 2826325500,
+ 2828857320,
+ 2831390340,
+ 2833928160,
+ 2836473060,
+ 2839025640,
+ 2841585360,
+ 2844151080,
+ 2846720580,
+ 2849289840,
+ 2851854120,
+ 2854409940,
+ 2856957000,
+ 2859497520,
+ 2862035040,
+ 2864572920,
+ 2867113140,
+ 2869656600,
+ 2872203960,
+ 2874756300,
+ 2877314220,
+ 2879876940,
+ 2882441700,
+ 2885004720,
+ 2887563300,
+ 2890116780,
+ 2892665940,
+ 2895212040,
+ 2897756040,
+ 2900298900,
+ 2902841700,
+ 2905385940,
+ 2907933000,
+ 2910483480,
+ 2913037200,
+ 2915593500,
+ 2918151960,
+ 2920711740,
+ 2923271880,
+ 2925830340,
+ 2928385200,
+ 2930935800,
+ 2933482800,
+ 2936027640,
+ 2938571460,
+ 2941114680,
+ 2943657480,
+ 2946201240,
+ 2948748300,
+ 2951301000,
+ 2953860120,
+ 2956424160,
+ 2958989400,
+ 2961552240,
+ 2964110880,
+ 2966664840,
+ 2969213940,
+ 2971758120,
+ 2974297440,
+ 2976833520,
+ 2979369540,
+ 2981909520,
+ 2984456580,
+ 2987012280,
+ 2989575660,
+ 2992144080,
+ 2994714000,
+ 2997281760,
+ 2999843700,
+ 3002397240,
+ 3004941540,
+ 3007478340,
+ 3010010880,
+ 3012543060,
+ 3015078480,
+ 3017620080,
+ 3020170020,
+ 3022729620,
+ 3025298400,
+ 3027873240,
+ 3030448140,
+ 3033016260,
+ 3035573460,
+ 3038119560,
+ 3040657260,
+ 3043189980,
+ 3045721020,
+ 3048253260,
+ 3050789940,
+ 3053334420,
+ 3055889700,
+ 3058456740,
+ 3061032180,
+ 3063609060,
+ 3066180240,
+ 3068741460,
+ 3071292120,
+ 3073833840,
+ 3076368840,
+ 3078900000,
+ 3081430740,
+ 3083964960,
+ 3086506680,
+ 3089058780,
+ 3091621440,
+ 3094192020,
+ 3096765600,
+ 3099337080,
+ 3101902260,
+ 3104458560,
+ 3107005260,
+ 3109543740,
+ 3112077180,
+ 3114609660,
+ 3117145320,
+ 3119686980,
+ 3122235840,
+ 3124791960,
+ 3127354320,
+ 3129921480,
+ 3132490500,
+ 3135057180,
+ 3137617260,
+ 3140168880,
+ 3142712820,
+ 3145252140,
+ 3147790320,
+ 3150329880,
+ 3152872080,
+ 3155417520,
+ 3157966920,
+ 3160521240,
+ 3163080780,
+ 3165643980,
+ 3168207540,
+ 3170768340,
+ 3173324700,
+ 3175876560,
+ 3178425000,
+ 3180970860,
+ 3183514980,
+ 3186058080,
+ 3188601480,
+ 3191146560,
+ 3193694400,
+ 3196245480,
+ 3198799560,
+ 3201356280,
+ 3203915280,
+ 3206475720,
+ 3209036040,
+ 3211593960,
+ 3214147740,
+ 3216697200,
+ 3219243360,
+ 3221787480,
+ 3224330340,
+ 3226872420,
+ 3229414560,
+ 3231958860,
+ 3234507840,
+ 3237063540,
+ 3239625720,
+ 3242191560,
+ 3244756980,
+ 3247318860,
+ 3249875880,
+ 3252427680,
+ 3254974140,
+ 3257515140,
+ 3260051880,
+ 3262586760,
+ 3265123680,
+ 3267666480,
+ 3270217680,
+ 3272777640,
+ 3275344560,
+ 3277915020,
+ 3280485060,
+ 3283050780,
+ 3285608820,
+ 3288157440,
+ 3290697180,
+ 3293230860,
+ 3295762260,
+ 3298295280,
+ 3300833280,
+ 3303378900,
+ 3305933820,
+ 3308498580,
+ 3311071500,
+ 3313647660,
+ 3316220040,
+ 3318782880,
+ 3321334020,
+ 3323875200,
+ 3326409660,
+ 3328940940,
+ 3331472220,
+ 3334006380,
+ 3336546780,
+ 3339096660,
+ 3341658180,
+ 3344230200,
+ 3346807320,
+ 3349381860,
+ 3351947940,
+ 3354503520,
+ 3357049260,
+ 3359587200,
+ 3362120040,
+ 3364650840,
+ 3367183200,
+ 3369721140,
+ 3372268200,
+ 3374825880,
+ 3377392800,
+ 3379965000,
+ 3382537500,
+ 3385105680,
+ 3387666360,
+ 3390217740,
+ 3392760240,
+ 3395296080,
+ 3397829040,
+ 3400363320,
+ 3402902280,
+ 3405447900,
+ 3408000420,
+ 3410559360,
+ 3413123520,
+ 3415691160,
+ 3418258860,
+ 3420822300,
+ 3423378240,
+ 3425926140,
+ 3428467980,
+ 3431007120,
+ 3433546620,
+ 3436088100,
+ 3438632220,
+ 3441179340,
+ 3443730480,
+ 3446286480,
+ 3448847100,
+ 3451410060,
+ 3453972060,
+ 3456530760,
+ 3459085320,
+ 3461636280,
+ 3464184360,
+ 3466730220,
+ 3469274340,
+ 3471817620,
+ 3474361440,
+ 3476907120,
+ 3479455620,
+ 3482007180,
+ 3484561680,
+ 3487119060,
+ 3489678960,
+ 3492240240,
+ 3494800620,
+ 3497357700,
+ 3499910220,
+ 3502458420,
+ 3505003440,
+ 3507546600,
+ 3510088380,
+ 3512629440,
+ 3515171340,
+ 3517716900,
+ 3520268700,
+ 3522827880,
+ 3525392940,
+ 3527959980,
+ 3530524920,
+ 3533085240,
+ 3535640040,
+ 3538189200,
+ 3540732540,
+ 3543270600,
+ 3545805180,
+ 3548339880,
+ 3550878780,
+ 3553425300,
+ 3555981120,
+ 3558545400,
+ 3561115260,
+ 3563686740,
+ 3566255700,
+ 3568818180,
+ 3571371480,
+ 3573915060,
+ 3576450780,
+ 3578982300,
+ 3581513640,
+ 3584048580,
+ 3586590120,
+ 3589140480,
+ 3591700740,
+ 3594270360,
+ 3596846040,
+ 3599421420,
+ 3601989540,
+ 3604546320,
+ 3607091760,
+ 3609628800,
+ 3612160980,
+ 3614691840,
+ 3617224320,
+ 3619761480,
+ 3622306500,
+ 3624862380,
+ 3627429720,
+ 3630005100,
+ 3632581620,
+ 3635152140,
+ 3637712640,
+ 3640262820,
+ 3642804300,
+ 3645339420,
+ 3647871120,
+ 3650402580,
+ 3652937760,
+ 3655480260,
+ 3658032780,
+ 3660595320,
+ 3663165060,
+ 3665737440,
+ 3668307660,
+ 3670871880,
+ 3673427820,
+ 3675974760,
+ 3678513960,
+ 3681048420,
+ 3683582220,
+ 3686119140,
+ 3688661700,
+ 3691210920,
+ 3693766440,
+ 3696327480,
+ 3698892720,
+ 3701460000,
+ 3704025480,
+ 3706585260,
+ 3709137420,
+ 3711682560,
+ 3714223500,
+ 3716763420,
+ 3719304480,
+ 3721847640,
+ 3724393200,
+ 3726941760,
+ 3729494400,
+ 3732051780,
+ 3734612880,
+ 3737175060,
+ 3739735500,
+ 3742292580,
+ 3744846060,
+ 3747396480,
+ 3749944380,
+ 3752490060,
+ 3755034000,
+ 3757577280,
+ 3760121280,
+ 3762667380,
+ 3765216300,
+ 3767768400,
+ 3770323800,
+ 3772882380,
+ 3775443540,
+ 3778005480,
+ 3780565560,
+ 3783121500,
+ 3785672400,
+ 3788219160,
+ 3790762920,
+ 3793304640,
+ 3795845100,
+ 3798385440,
+ 3800928060,
+ 3803475960,
+ 3806031240,
+ 3808594020,
+ 3811161360,
+ 3813728820,
+ 3816292560,
+ 3818850780,
+ 3821402940,
+ 3823948860,
+ 3826488840,
+ 3829024140,
+ 3831557640,
+ 3834093420,
+ 3836635500,
+ 3839186520,
+ 3841747140,
+ 3844315200,
+ 3846887160,
+ 3849458520,
+ 3852025020,
+ 3854583120,
+ 3857131080,
+ 3859669920,
+ 3862202580,
+ 3864733140,
+ 3867265620,
+ 3869803500,
+ 3872349360,
+ 3874904880,
+ 3877470420,
+ 3880044120,
+ 3882620760,
+ 3885193260,
+ 3887755740,
+ 3890306340,
+ 3892846800,
+ 3895380780,
+ 3897911820,
+ 3900443220,
+ 3902977800,
+ 3905518800,
+ 3908069280,
+ 3910631220,
+ 3913203240,
+ 3915779940,
+ 3918353820,
+ 3920919240,
+ 3923474220,
+ 3926019660,
+ 3928557660,
+ 3931090920,
+ 3933622440,
+ 3936155700,
+ 3938694600,
+ 3941242260,
+ 3943799940,
+ 3946366260,
+ 3948937320,
+ 3951508380,
+ 3954075420,
+ 3956635440,
+ 3959186880,
+ 3961729980,
+ 3964266900,
+ 3966801120,
+ 3969336720,
+ 3971876880,
+ 3974423040,
+ 3976975380,
+ 3979533180,
+ 3982095600,
+ 3984661260,
+ 3987227400,
+ 3989790120,
+ 3992346300,
+ 3994895280,
+ 3997438680,
+ 3999979560,
+ 4002520740,
+ 4005063420,
+ 4007607960,
+ 4010154600,
+ 4012704300,
+ 4015258320,
+ 4017816720,
+ 4020377940,
+ 4022939220,
+ 4025498220,
+ 4028054100,
+ 4030606920,
+ 4033157100,
+ 4035704760,
+ 4038250020,
+ 4040793540,
+ 4043336640,
+ 4045880820,
+ 4048427280,
+ 4050976800,
+ 4053529680,
+ 4056086220,
+ 4058646360,
+ 4061208960,
+ 4063771380,
+ 4066330620,
+ 4068884940,
+ 4071434040,
+ 4073979120,
+ 4076521380,
+ 4079061660,
+ 4081600980,
+ 4084141260,
+ 4086685440,
+ 4089236520,
+ 4091795880,
+ 4094362140,
+ 4096931040,
+ 4099497900,
+ 4102059660,
]
@@ -226,8 +1256,10 @@ def get_last_full_moon(d):
idx = bisect.bisect_right(fullmoons, now)
if idx in [0, len(fullmoons)]:
raise ValueError(
- 'watson has only full moon dates from year 2000 to 2099, not {}'
- .format(d.year))
+ "watson has only full moon dates from year 2000 to 2099, not {}".format(
+ d.year
+ )
+ )
last = fullmoons[idx - 1]
return arrow.get(last)
diff --git a/watson/utils.py b/watson/utils.py
index c276860d..6815800f 100644
--- a/watson/utils.py
+++ b/watson/utils.py
@@ -18,7 +18,7 @@
def create_watson():
- return _watson.Watson(config_dir=os.environ.get('WATSON_DIR'))
+ return _watson.Watson(config_dir=os.environ.get("WATSON_DIR"))
def confirm_project(project, watson_projects):
@@ -29,8 +29,7 @@ def confirm_project(project, watson_projects):
Returns True on accept and raises click.exceptions.Abort on reject
"""
if project not in watson_projects:
- msg = ("Project '%s' does not exist yet. Create it?"
- % style('project', project))
+ msg = "Project '%s' does not exist yet. Create it?" % style("project", project)
click.confirm(msg, abort=True)
return True
@@ -43,7 +42,7 @@ def confirm_tags(tags, watson_tags):
"""
for tag in tags:
if tag not in watson_tags:
- msg = "Tag '%s' does not exist yet. Create it?" % style('tag', tag)
+ msg = "Tag '%s' does not exist yet. Create it?" % style("tag", tag)
click.confirm(msg, abort=True)
return True
@@ -51,24 +50,22 @@ def confirm_tags(tags, watson_tags):
def style(name, element):
def _style_tags(tags):
if not tags:
- return ''
+ return ""
- return '[{}]'.format(', '.join(
- style('tag', tag) for tag in tags
- ))
+ return "[{}]".format(", ".join(style("tag", tag) for tag in tags))
def _style_short_id(id):
- return style('id', id[:7])
+ return style("id", id[:7])
formats = {
- 'project': {'fg': 'magenta'},
- 'tags': _style_tags,
- 'tag': {'fg': 'blue'},
- 'time': {'fg': 'green'},
- 'error': {'fg': 'red'},
- 'date': {'fg': 'cyan'},
- 'short_id': _style_short_id,
- 'id': {'fg': 'white'}
+ "project": {"fg": "magenta"},
+ "tags": _style_tags,
+ "tag": {"fg": "blue"},
+ "time": {"fg": "green"},
+ "error": {"fg": "red"},
+ "date": {"fg": "cyan"},
+ "short_id": _style_short_id,
+ "id": {"fg": "white"},
}
fmt = formats.get(name, {})
@@ -92,17 +89,17 @@ def format_timedelta(delta):
if total >= 3600:
hours = seconds // 3600
- stems.append('{}h'.format(hours))
+ stems.append("{}h".format(hours))
seconds -= hours * 3600
if total >= 60:
mins = seconds // 60
- stems.append('{:02}m'.format(mins))
+ stems.append("{:02}m".format(mins))
seconds -= mins * 60
- stems.append('{:02}s'.format(seconds))
+ stems.append("{:02}s".format(seconds))
- return ('-' if neg else '') + ' '.join(stems)
+ return ("-" if neg else "") + " ".join(stems)
def sorted_groupby(iterator, key, reverse=False):
@@ -118,12 +115,17 @@ def options(opt_list):
Wrapper for the `value_proc` field in `click.prompt`, which validates
that the user response is part of the list of accepted responses.
"""
+
def value_proc(user_input):
if user_input in opt_list:
return user_input
else:
- raise UsageError("Response should be one of [{}]".format(
- ','.join(str(x) for x in opt_list)))
+ raise UsageError(
+ "Response should be one of [{}]".format(
+ ",".join(str(x) for x in opt_list)
+ )
+ )
+
return value_proc
@@ -141,7 +143,7 @@ def get_frame_from_argument(watson, arg):
return watson.frames[index]
except IndexError:
raise click.ClickException(
- style('error', "No frame found for index {}.".format(arg))
+ style("error", "No frame found for index {}.".format(arg))
)
except (ValueError, TypeError):
pass
@@ -150,9 +152,10 @@ def get_frame_from_argument(watson, arg):
try:
return watson.frames[arg]
except KeyError:
- raise click.ClickException("{} {}.".format(
- style('error', "No frame found with id"),
- style('short_id', arg))
+ raise click.ClickException(
+ "{} {}.".format(
+ style("error", "No frame found with id"), style("short_id", arg)
+ )
)
@@ -167,21 +170,21 @@ def get_start_time_for_period(period):
weekday = now.weekday()
- if period == 'day':
+ if period == "day":
start_time = arrow.Arrow(year, month, day)
- elif period == 'week':
+ elif period == "week":
start_time = arrow.Arrow.fromdate(now.shift(days=-weekday).date())
- elif period == 'month':
+ elif period == "month":
start_time = arrow.Arrow(year, month, 1)
- elif period == 'luna':
+ elif period == "luna":
start_time = get_last_full_moon(now)
- elif period == 'year':
+ elif period == "year":
start_time = arrow.Arrow(year, 1, 1)
- elif period == 'all':
+ elif period == "all":
# approximately timestamp `0`
start_time = arrow.Arrow(1970, 1, 1)
else:
- raise ValueError('Unsupported period value: {}'.format(period))
+ raise ValueError("Unsupported period value: {}".format(period))
return start_time
@@ -191,9 +194,20 @@ def apply_weekday_offset(start_time, week_start):
Apply the offset required to move the start date `start_time` of a week
starting on Monday to that of a week starting on `week_start`.
"""
- weekdays = dict(zip(
- ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday",
- "sunday"], range(0, 7)))
+ weekdays = dict(
+ zip(
+ [
+ "monday",
+ "tuesday",
+ "wednesday",
+ "thursday",
+ "friday",
+ "saturday",
+ "sunday",
+ ],
+ range(0, 7),
+ )
+ )
new_start = week_start.lower()
if new_start not in weekdays:
@@ -208,13 +222,15 @@ def make_json_writer(func, *args, **kwargs):
Return a function that receives a file-like object and writes the return
value of func(*args, **kwargs) as JSON to it.
"""
+
def writer(f):
dump = json.dumps(func(*args, **kwargs), indent=1, ensure_ascii=False)
f.write(dump)
+
return writer
-def safe_save(path, content, ext='.bak'):
+def safe_save(path, content, ext=".bak"):
"""
Save given content to file at given path safely.
@@ -230,7 +246,7 @@ def safe_save(path, content, ext='.bak'):
the temporary file moved into its place.
"""
- tmpfp = tempfile.NamedTemporaryFile(mode='w+', delete=False)
+ tmpfp = tempfile.NamedTemporaryFile(mode="w+", delete=False)
try:
with tmpfp:
if isinstance(content, str):
@@ -260,9 +276,11 @@ def deduplicate(sequence):
Leaves the input sequence unaltered.
"""
- return [element
- for index, element in enumerate(sequence)
- if element not in sequence[:index]]
+ return [
+ element
+ for index, element in enumerate(sequence)
+ if element not in sequence[:index]
+ ]
def parse_tags(values_list):
@@ -272,14 +290,27 @@ def parse_tags(values_list):
Find all the tags starting by a '+', even if there are spaces in them,
then strip each tag and filter out the empty ones
"""
- return list(filter(None, map(operator.methodcaller('strip'), (
- # We concatenate the word with the '+' to the following words
- # not starting with a '+'
- w[1:] + ' ' + ' '.join(itertools.takewhile(
- lambda s: not s.startswith('+'), values_list[i + 1:]
- ))
- for i, w in enumerate(values_list) if w.startswith('+')
- )))) # pile of pancakes !
+ return list(
+ filter(
+ None,
+ map(
+ operator.methodcaller("strip"),
+ (
+ # We concatenate the word with the '+' to the following words
+ # not starting with a '+'
+ w[1:]
+ + " "
+ + " ".join(
+ itertools.takewhile(
+ lambda s: not s.startswith("+"), values_list[(i + 1) :]
+ )
+ )
+ for i, w in enumerate(values_list)
+ if w.startswith("+")
+ ),
+ ),
+ )
+ ) # pile of pancakes !
def frames_to_json(frames):
@@ -292,13 +323,15 @@ def frames_to_json(frames):
.. seealso:: :class:`Frame`
"""
log = [
- co.OrderedDict([
- ('id', frame.id),
- ('start', frame.start.isoformat()),
- ('stop', frame.stop.isoformat()),
- ('project', frame.project),
- ('tags', frame.tags),
- ])
+ co.OrderedDict(
+ [
+ ("id", frame.id),
+ ("start", frame.start.isoformat()),
+ ("stop", frame.stop.isoformat()),
+ ("project", frame.project),
+ ("tags", frame.tags),
+ ]
+ )
for frame in frames
]
return json.dumps(log, indent=4, sort_keys=True)
@@ -314,13 +347,15 @@ def frames_to_csv(frames):
.. seealso:: :class:`Frame`
"""
entries = [
- co.OrderedDict([
- ('id', frame.id[:7]),
- ('start', frame.start.format('YYYY-MM-DD HH:mm:ss')),
- ('stop', frame.stop.format('YYYY-MM-DD HH:mm:ss')),
- ('project', frame.project),
- ('tags', ', '.join(frame.tags)),
- ])
+ co.OrderedDict(
+ [
+ ("id", frame.id[:7]),
+ ("start", frame.start.format("YYYY-MM-DD HH:mm:ss")),
+ ("stop", frame.stop.format("YYYY-MM-DD HH:mm:ss")),
+ ("project", frame.project),
+ ("tags", ", ".join(frame.tags)),
+ ]
+ )
for frame in frames
]
return build_csv(entries)
@@ -336,7 +371,7 @@ def build_csv(entries):
if entries:
header = entries[0].keys()
else:
- return ''
+ return ""
memfile = StringIO()
writer = csv.DictWriter(memfile, header, lineterminator=os.linesep)
writer.writeheader()
@@ -366,24 +401,28 @@ def flatten_report_for_csv(report):
of the report.
"""
result = []
- datetime_from = report['timespan']['from'].format('YYYY-MM-DD HH:mm:ss')
- datetime_to = report['timespan']['to'].format('YYYY-MM-DD HH:mm:ss')
- for project in report['projects']:
- result.append({
- 'from': datetime_from,
- 'to': datetime_to,
- 'project': project['name'],
- 'tag': '',
- 'time': project['time']
- })
- for tag in project['tags']:
- result.append({
- 'from': datetime_from,
- 'to': datetime_to,
- 'project': project['name'],
- 'tag': tag['name'],
- 'time': tag['time']
- })
+ datetime_from = report["timespan"]["from"].format("YYYY-MM-DD HH:mm:ss")
+ datetime_to = report["timespan"]["to"].format("YYYY-MM-DD HH:mm:ss")
+ for project in report["projects"]:
+ result.append(
+ {
+ "from": datetime_from,
+ "to": datetime_to,
+ "project": project["name"],
+ "tag": "",
+ "time": project["time"],
+ }
+ )
+ for tag in project["tags"]:
+ result.append(
+ {
+ "from": datetime_from,
+ "to": datetime_to,
+ "project": project["name"],
+ "tag": tag["name"],
+ "time": tag["time"],
+ }
+ )
return result
diff --git a/watson/watson.py b/watson/watson.py
index 7ab7da12..3c062d1a 100644
--- a/watson/watson.py
+++ b/watson/watson.py
@@ -47,22 +47,21 @@ def __init__(self, **kwargs):
self._config = None
self._config_changed = False
- self._dir = (kwargs.pop('config_dir', None) or
- click.get_app_dir('watson'))
+ self._dir = kwargs.pop("config_dir", None) or click.get_app_dir("watson")
- self.config_file = os.path.join(self._dir, 'config')
- self.frames_file = os.path.join(self._dir, 'frames')
- self.state_file = os.path.join(self._dir, 'state')
- self.last_sync_file = os.path.join(self._dir, 'last_sync')
+ self.config_file = os.path.join(self._dir, "config")
+ self.frames_file = os.path.join(self._dir, "frames")
+ self.state_file = os.path.join(self._dir, "state")
+ self.last_sync_file = os.path.join(self._dir, "last_sync")
- if 'frames' in kwargs:
- self.frames = kwargs['frames']
+ if "frames" in kwargs:
+ self.frames = kwargs["frames"]
- if 'current' in kwargs:
- self.current = kwargs['current']
+ if "current" in kwargs:
+ self.current = kwargs["current"]
- if 'last_sync' in kwargs:
- self.last_sync = kwargs['last_sync']
+ if "last_sync" in kwargs:
+ self.last_sync = kwargs["last_sync"]
def _load_json_file(self, filename, type=dict):
"""
@@ -82,19 +81,15 @@ def _load_json_file(self, filename, type=dict):
if os.path.getsize(filename) == 0:
return type()
else:
- raise WatsonError(
- "Invalid JSON file {}: {}".format(filename, e)
- )
+ raise WatsonError("Invalid JSON file {}: {}".format(filename, e))
except Exception as e:
raise WatsonError(
- "Unexpected error while loading JSON file {}: {}".format(
- filename, e
- )
+ "Unexpected error while loading JSON file {}: {}".format(filename, e)
)
def _parse_date(self, date):
"""Returns Arrow object from timestamp."""
- return arrow.Arrow.utcfromtimestamp(date).to('local')
+ return arrow.Arrow.utcfromtimestamp(date).to("local")
def _format_date(self, date):
"""Returns timestamp from string timestamp or Arrow object."""
@@ -113,8 +108,7 @@ def config(self):
config = ConfigParser()
config.read(self.config_file)
except CFGParserError as e:
- raise ConfigurationError(
- "Cannot parse config file: {}".format(e))
+ raise ConfigurationError("Cannot parse config file: {}".format(e))
self._config = config
@@ -139,9 +133,9 @@ def save(self):
if self._current is not None and self._old_state != self._current:
if self.is_started:
current = {
- 'project': self.current['project'],
- 'start': self._format_date(self.current['start']),
- 'tags': self.current['tags'],
+ "project": self.current["project"],
+ "start": self._format_date(self.current["start"]),
+ "tags": self.current["tags"],
}
else:
current = {}
@@ -150,19 +144,18 @@ def save(self):
self._old_state = current
if self._frames is not None and self._frames.changed:
- safe_save(self.frames_file,
- make_json_writer(self.frames.dump))
+ safe_save(self.frames_file, make_json_writer(self.frames.dump))
if self._config_changed:
safe_save(self.config_file, self.config.write)
if self._last_sync is not None:
- safe_save(self.last_sync_file,
- make_json_writer(self._format_date, self.last_sync))
+ safe_save(
+ self.last_sync_file,
+ make_json_writer(self._format_date, self.last_sync),
+ )
except OSError as e:
- raise WatsonError(
- "Impossible to write {}: {}".format(e.filename, e)
- )
+ raise WatsonError("Impossible to write {}: {}".format(e.filename, e))
@property
def frames(self):
@@ -187,7 +180,7 @@ def current(self):
@current.setter
def current(self, value):
- if not value or 'project' not in value:
+ if not value or "project" not in value:
self._current = {}
if self._old_state is None:
@@ -195,15 +188,15 @@ def current(self, value):
return
- start = value.get('start', arrow.now())
+ start = value.get("start", arrow.now())
if not isinstance(start, arrow.Arrow):
start = self._parse_date(start)
self._current = {
- 'project': value['project'],
- 'start': start,
- 'tags': value.get('tags') or []
+ "project": value["project"],
+ "start": start,
+ "tags": value.get("tags") or [],
}
if self._old_state is None:
@@ -212,9 +205,7 @@ def current(self, value):
@property
def last_sync(self):
if self._last_sync is None:
- self.last_sync = self._load_json_file(
- self.last_sync_file, type=int
- )
+ self.last_sync = self._load_json_file(self.last_sync_file, type=int)
return self._last_sync
@@ -239,22 +230,19 @@ def add(self, project, from_date, to_date, tags):
if from_date > to_date:
raise WatsonError("Task cannot end before it starts.")
- default_tags = self.config.getlist('default_tags', project)
+ default_tags = self.config.getlist("default_tags", project)
tags = (tags or []) + default_tags
frame = self.frames.add(project, from_date, to_date, tags=tags)
return frame
- def start(self, project, tags=None, restart=False, start_at=None,
- gap=True):
+ def start(self, project, tags=None, restart=False, start_at=None, gap=True):
if self.is_started:
raise WatsonError(
- "Project {} is already started.".format(
- self.current['project']
- )
+ "Project {} is already started.".format(self.current["project"])
)
- default_tags = self.config.getlist('default_tags', project)
+ default_tags = self.config.getlist("default_tags", project)
if not restart:
tags = (tags or []) + default_tags
@@ -265,16 +253,15 @@ def start(self, project, tags=None, restart=False, start_at=None,
# and previous frames exist
stop_of_prev_frame = self.frames[-1].stop
if start_at < stop_of_prev_frame:
- raise WatsonError('Task cannot start before the previous task '
- 'ends.')
+ raise WatsonError("Task cannot start before the previous task " "ends.")
if start_at > arrow.now():
- raise WatsonError('Task cannot start in the future.')
+ raise WatsonError("Task cannot start in the future.")
- new_frame = {'project': project, 'tags': deduplicate(tags)}
- new_frame['start'] = start_at
+ new_frame = {"project": project, "tags": deduplicate(tags)}
+ new_frame["start"] = start_at
if not gap:
stop_of_prev_frame = self.frames[-1].stop
- new_frame['start'] = stop_of_prev_frame
+ new_frame["start"] = stop_of_prev_frame
self.current = new_frame
return self.current
@@ -291,14 +278,12 @@ def stop(self, stop_at=None):
# stop function and calling it, the value of `stop_at` could be
# outdated if defined using a default argument.
stop_at = arrow.now()
- if old['start'] > stop_at:
- raise WatsonError('Task cannot end before it starts.')
+ if old["start"] > stop_at:
+ raise WatsonError("Task cannot end before it starts.")
if stop_at > arrow.now():
- raise WatsonError('Task cannot end in the future.')
+ raise WatsonError("Task cannot end in the future.")
- frame = self.frames.add(
- old['project'], old['start'], stop_at, tags=old['tags']
- )
+ frame = self.frames.add(old["project"], old["start"], stop_at, tags=old["tags"])
self.current = None
return frame
@@ -316,26 +301,23 @@ def projects(self):
"""
Return the list of all the existing projects, sorted by name.
"""
- return sorted(set(self.frames['project']))
+ return sorted(set(self.frames["project"]))
@property
def tags(self):
"""
Return the list of the tags, sorted by name.
"""
- return sorted(set(tag for tags in self.frames['tags'] for tag in tags))
+ return sorted(set(tag for tags in self.frames["tags"] for tag in tags))
def _get_request_info(self, route):
config = self.config
- dest = config.get('backend', 'url')
- token = config.get('backend', 'token')
+ dest = config.get("backend", "url")
+ token = config.get("backend", "token")
if dest and token:
- dest = "{}/{}/".format(
- dest.rstrip('/'),
- route.strip('/')
- )
+ dest = "{}/{}/".format(dest.rstrip("/"), route.strip("/"))
else:
raise ConfigurationError(
"You must specify a remote URL (backend.url) and a token "
@@ -343,8 +325,8 @@ def _get_request_info(self, route):
)
headers = {
- 'content-type': 'application/json',
- 'Authorization': "Token {}".format(token)
+ "content-type": "application/json",
+ "Authorization": "Token {}".format(token),
}
return dest, headers
@@ -352,8 +334,9 @@ def _get_request_info(self, route):
def _get_remote_projects(self):
# import when required in order to reduce watson response time (#312)
import requests
- if not hasattr(self, '_remote_projects'):
- dest, headers = self._get_request_info('projects')
+
+ if not hasattr(self, "_remote_projects"):
+ dest, headers = self._get_request_info("projects")
try:
response = requests.get(dest, headers=headers)
@@ -368,15 +351,16 @@ def _get_remote_projects(self):
"server: {}".format(response.json())
)
- return self._remote_projects['projects']
+ return self._remote_projects["projects"]
def pull(self):
import requests
- dest, headers = self._get_request_info('frames')
+
+ dest, headers = self._get_request_info("frames")
try:
response = requests.get(
- dest, params={'last_sync': self.last_sync}, headers=headers
+ dest, params={"last_sync": self.last_sync}, headers=headers
)
assert response.status_code == 200
except requests.ConnectionError:
@@ -390,31 +374,34 @@ def pull(self):
frames = response.json() or ()
for frame in frames:
- frame_id = uuid.UUID(frame['id']).hex
+ frame_id = uuid.UUID(frame["id"]).hex
self.frames[frame_id] = (
- frame['project'],
- frame['begin_at'],
- frame['end_at'],
- frame['tags']
+ frame["project"],
+ frame["begin_at"],
+ frame["end_at"],
+ frame["tags"],
)
return frames
def push(self, last_pull):
import requests
- dest, headers = self._get_request_info('frames/bulk')
+
+ dest, headers = self._get_request_info("frames/bulk")
frames = []
for frame in self.frames:
if last_pull > frame.updated_at > self.last_sync:
- frames.append({
- 'id': uuid.UUID(frame.id).urn,
- 'begin_at': str(frame.start.to('utc')),
- 'end_at': str(frame.stop.to('utc')),
- 'project': frame.project,
- 'tags': frame.tags
- })
+ frames.append(
+ {
+ "id": uuid.UUID(frame.id).urn,
+ "begin_at": str(frame.start.to("utc")),
+ "end_at": str(frame.stop.to("utc")),
+ "project": frame.project,
+ "tags": frame.tags,
+ }
+ )
try:
response = requests.post(dest, json.dumps(frames), headers=headers)
@@ -424,17 +411,15 @@ def push(self, last_pull):
except AssertionError:
raise WatsonError(
"An error occurred with the remote server (status: {}). "
- "Response was:\n{}".format(
- response.status_code,
- response.text
- )
+ "Response was:\n{}".format(response.status_code, response.text)
)
return frames
def merge_report(self, frames_with_conflict):
- conflict_file_frames = Frames(self._load_json_file(
- frames_with_conflict, type=list))
+ conflict_file_frames = Frames(
+ self._load_json_file(frames_with_conflict, type=list)
+ )
conflicting = []
merging = []
@@ -455,19 +440,33 @@ def merge_report(self, frames_with_conflict):
def _validate_report_options(self, filtrate, ignored):
return not bool(
- filtrate and ignored and set(filtrate).intersection(set(ignored)))
-
- def report(self, from_, to, current=None, projects=None, tags=None,
- ignore_projects=None, ignore_tags=None, year=None,
- month=None, week=None, day=None, luna=None, all=None,
- include_partial_frames=False):
- for start_time in (_ for _ in [day, week, month, year, luna, all]
- if _ is not None):
+ filtrate and ignored and set(filtrate).intersection(set(ignored))
+ )
+
+ def report(
+ self,
+ from_,
+ to,
+ current=None,
+ projects=None,
+ tags=None,
+ ignore_projects=None,
+ ignore_tags=None,
+ year=None,
+ month=None,
+ week=None,
+ day=None,
+ luna=None,
+ all=None,
+ include_partial_frames=False,
+ ):
+ for start_time in (
+ _ for _ in [day, week, month, year, luna, all] if _ is not None
+ ):
from_ = start_time
if not self._validate_report_options(projects, ignore_projects):
- raise WatsonError(
- "given projects can't be ignored at the same time")
+ raise WatsonError("given projects can't be ignored at the same time")
if not self._validate_report_options(tags, ignore_tags):
raise WatsonError("given tags can't be ignored at the same time")
@@ -476,76 +475,80 @@ def report(self, from_, to, current=None, projects=None, tags=None,
raise WatsonError("'from' must be anterior to 'to'")
if current is None:
- current = self.config.getboolean('options', 'report_current')
+ current = self.config.getboolean("options", "report_current")
if self.current and current:
cur = self.current
- self.frames.add(cur['project'], cur['start'], arrow.utcnow(),
- cur['tags'], id="current")
+ self.frames.add(
+ cur["project"], cur["start"], arrow.utcnow(), cur["tags"], id="current"
+ )
span = self.frames.span(from_, to)
frames_by_project = sorted_groupby(
self.frames.filter(
- projects=projects or None, tags=tags or None,
+ projects=projects or None,
+ tags=tags or None,
ignore_projects=ignore_projects or None,
ignore_tags=ignore_tags or None,
- span=span, include_partial_frames=include_partial_frames,
+ span=span,
+ include_partial_frames=include_partial_frames,
),
- operator.attrgetter('project')
+ operator.attrgetter("project"),
)
if self.current and current:
- del self.frames['current']
+ del self.frames["current"]
total = datetime.timedelta()
report = {
- 'timespan': {
- 'from': span.start,
- 'to': span.stop,
- },
- 'projects': []
- }
+ "timespan": {
+ "from": span.start,
+ "to": span.stop,
+ },
+ "projects": [],
+ }
for project, frames in frames_by_project:
frames = tuple(frames)
delta = reduce(
- operator.add,
- (f.stop - f.start for f in frames),
- datetime.timedelta()
+ operator.add, (f.stop - f.start for f in frames), datetime.timedelta()
)
total += delta
project_report = {
- 'name': project,
- 'time': delta.total_seconds(),
- 'tags': []
+ "name": project,
+ "time": delta.total_seconds(),
+ "tags": [],
}
if tags is None:
tags = []
tags_to_print = sorted(
- set(tag for frame in frames for tag in frame.tags
- if tag in tags or not tags)
+ set(
+ tag
+ for frame in frames
+ for tag in frame.tags
+ if tag in tags or not tags
+ )
)
for tag in tags_to_print:
delta = reduce(
operator.add,
(f.stop - f.start for f in frames if tag in f.tags),
- datetime.timedelta()
+ datetime.timedelta(),
)
- project_report['tags'].append({
- 'name': tag,
- 'time': delta.total_seconds()
- })
+ project_report["tags"].append(
+ {"name": tag, "time": delta.total_seconds()}
+ )
- report['projects'].append(project_report)
+ report["projects"].append(project_report)
- report['time'] = total.total_seconds()
+ report["time"] = total.total_seconds()
return report
def rename_project(self, old_project, new_project):
@@ -558,8 +561,7 @@ def rename_project(self, old_project, new_project):
for frame in self.frames:
if frame.project == old_project:
self.frames[frame.id] = frame._replace(
- project=new_project,
- updated_at=updated_at
+ project=new_project, updated_at=updated_at
)
self.frames.changed = True
@@ -576,7 +578,7 @@ def rename_tag(self, old_tag, new_tag):
if old_tag in frame.tags:
self.frames[frame.id] = frame._replace(
tags=[new_tag if t == old_tag else t for t in frame.tags],
- updated_at=updated_at
+ updated_at=updated_at,
)
self.frames.changed = True