Repository: jazzband/Watson (Python)

Issue:
Breaking changes with arrow 1.0.0
## Problem Description

I installed *td-watson* in a fresh environment and received an error when running:

```
$ docker run -it --rm python:latest /bin/bash
root@4156557dc8cf:/# pip install td-watson
...
root@4156557dc8cf:/# watson status
Traceback (most recent call last):
  File "/usr/local/bin/watson", line 5, in <module>
    from watson.__main__ import cli
  File "/usr/local/lib/python3.9/site-packages/watson/__main__.py", line 2, in <module>
    from watson import cli
  File "/usr/local/lib/python3.9/site-packages/watson/cli.py", line 441, in <module>
    _SHORTCUT_OPTIONS_VALUES = {
  File "/usr/local/lib/python3.9/site-packages/watson/cli.py", line 442, in <dictcomp>
    k: get_start_time_for_period(k) for k in _SHORTCUT_OPTIONS
  File "/usr/local/lib/python3.9/site-packages/watson/utils.py", line 193, in get_start_time_for_period
    start_time = get_last_full_moon(now)
  File "/usr/local/lib/python3.9/site-packages/watson/fullmoon.py", line 226, in get_last_full_moon
    idx = bisect.bisect_right(fullmoons, now)
TypeError: '<' not supported between instances of 'method' and 'int'
```

The *arrow library* was [updated on Feb 24, 2021](https://arrow.readthedocs.io/en/stable/releases.html) and there are [breaking changes](https://github.com/arrow-py/arrow/issues/832).

The default *td-watson* installation uses the new version,  *arrow 1.0.0*:

```
root@4156557dc8cf:/# pip freeze
arrow==1.0.0
certifi==2020.12.5
chardet==4.0.0
click==7.1.2
click-didyoumean==0.0.3
idna==2.10
python-dateutil==2.8.1
requests==2.25.1
six==1.15.0
td-watson==1.10.0
urllib3==1.26.3
```

## Workaround

Downgrade to *arrow 0.17.0*:

```
root@4156557dc8cf:/# pip uninstall arrow
Found existing installation: arrow 1.0.0
Uninstalling arrow-1.0.0:
  Would remove:
    /usr/local/lib/python3.9/site-packages/arrow-1.0.0.dist-info/*
    /usr/local/lib/python3.9/site-packages/arrow/*
Proceed (y/n)? y
  Successfully uninstalled arrow-1.0.0
root@4156557dc8cf:/# pip install arrow==0.17.0
```

And *td-watson* works again:

```
root@4156557dc8cf:/# watson status
No project started.
root@4156557dc8cf:/# watson start testing123
Starting project testing123 at 15:25
root@4156557dc8cf:/# watson stop
Stopping project testing123, started just now and stopped just now. (id: d056c4c)
```

## Quick fix

Update [requirements.txt](https://github.com/TailorDev/Watson/blob/master/requirements.txt) changing the line:
```
arrow>=0.15.6
```

To specify the exact version:
```
arrow==0.17.0
```

## Long-term solution

According to the [arrow 1.0.0 migration guide](https://github.com/arrow-py/arrow/issues/832):

> The .timestamp property has been removed to improve compatibility with datetime. Use .int_timestamp for the same results.
>
> >>> arrow.utcnow().int_timestamp
> 1608640233

I tried this and it appears to work.

The changes are:

```
diff watson/frames.py /usr/local/lib/python3.9/site-packages/watson/frames.py
38,40c38,40
<         start = self.start.to('utc').timestamp
<         stop = self.stop.to('utc').timestamp
<         updated_at = self.updated_at.timestamp
---
>         start = self.start.to('utc').int_timestamp
>         stop = self.stop.to('utc').int_timestamp
>         updated_at = self.updated_at.int_timestamp
diff watson/fullmoon.py /usr/local/lib/python3.9/site-packages/watson/fullmoon.py
225c225,226
<     now = d.timestamp
---
>     now = d.int_timestamp
diff watson/watson.py /usr/local/lib/python3.9/site-packages/watson/watson.py
104c104
<         return date.timestamp
---
>         return date.int_timestamp
```


Discussion:
Also need to update the tzinfo calls.

According to the [arrow 1.0.0 migration guide](https://github.com/arrow-py/arrow/issues/832).

>It's no longer possible to set the tzinfo property directly, instead use replace().
>>>> dt=arrow.utcnow()
>>>> dt.tzinfo=tz.gettz("US/Pacific")
>Traceback (most recent call last):
 > File "<stdin>", line 1, in <module>
>AttributeError: can't set attribute
>>>> dt.replace(tzinfo=tz.gettz("US/Pacific"))
><Arrow [2020-12-22T16:03:29.630250-08:00]>

The following seems to address this:

```
# diff watson/cli.py /usr/local/lib/python3.9/site-packages/watson/cli.py
84c84
<             date.tzinfo = tz.tzlocal()
---
>             date.replace(tzinfo=tz.tzlocal())
```
Looks wonderful. Can you submit a PR with your fix?
Thank you.  PR submitted.
:loudspeaker: The new `2.0.0` release including this fix is out! https://pypi.org/project/td-watson/2.0.0/ :fire: 



Patch:
diff --git a/.travis.yml b/.travis.yml
index 3f5c3165..88bee9a6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,10 +6,6 @@ matrix:
           os: linux
           dist: xenial
           env: TOXENV=flake8
-        - python: 3.5
-          os: linux
-          dist: trusty
-          env: TOXENV=py35
         - python: 3.6
           os: linux
           dist: trusty
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c27beffa..5ce4b9ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,9 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Python 3.8 support (#402)
 - Python 3.9 support (#402)
 
+### Changed
+
+- Upgrade to major arrow release 1.0.0 (#407)
+
 ### Removed
 
 - Python 2.7 support (#305).
+- Python 3.5 support (#407).
 
 ## [1.10.0] - 2020-07-03
 
diff --git a/requirements.txt b/requirements.txt
index f7b08ee8..4ab22204 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-arrow>=0.15.6
+arrow>=1.0.0
 click>=7.0
 click-didyoumean
 colorama; sys_platform == "win32"
diff --git a/setup.py b/setup.py
index feeea17f..22e16370 100644
--- a/setup.py
+++ b/setup.py
@@ -45,7 +45,7 @@ def parse_requirements(requirements, ignore=('setuptools',)):
     license='MIT',
     long_description=readme,
     install_requires=parse_requirements('requirements.txt'),
-    python_requires='>=3.5',
+    python_requires='>=3.6',
     tests_require=parse_requirements('requirements-dev.txt'),
     entry_points={
         'console_scripts': [
diff --git a/watson/cli.py b/watson/cli.py
index 34de207d..3ccbe355 100644
--- a/watson/cli.py
+++ b/watson/cli.py
@@ -81,7 +81,7 @@ def convert(self, value, param, ctx):
             # 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
             # timezone, not in UTC. Cf issue #16.
-            date.tzinfo = tz.tzlocal()
+            date = date.replace(tzinfo=tz.tzlocal())
             # Add an offset to match the week beginning specified in the
             # configuration
             if param.name == "week":
diff --git a/watson/frames.py b/watson/frames.py
index a99e3189..79037b88 100644
--- a/watson/frames.py
+++ b/watson/frames.py
@@ -35,9 +35,9 @@ def __new__(cls, start, stop, project, id, tags=None, updated_at=None,):
         )
 
     def dump(self):
-        start = self.start.to('utc').timestamp
-        stop = self.stop.to('utc').timestamp
-        updated_at = self.updated_at.timestamp
+        start = self.start.to('utc').int_timestamp
+        stop = self.stop.to('utc').int_timestamp
+        updated_at = self.updated_at.int_timestamp
 
         return (start, stop, self.project, self.id, self.tags, updated_at)
 
diff --git a/watson/fullmoon.py b/watson/fullmoon.py
index 7ca61f5a..5abd5535 100644
--- a/watson/fullmoon.py
+++ b/watson/fullmoon.py
@@ -222,7 +222,7 @@ def get_last_full_moon(d):
     Raises ValueError if the d value is not between 2000 - 2099
     """
 
-    now = d.timestamp
+    now = d.int_timestamp
     idx = bisect.bisect_right(fullmoons, now)
     if idx in [0, len(fullmoons)]:
         raise ValueError(
diff --git a/watson/watson.py b/watson/watson.py
index 9b25a734..7ab7da12 100644
--- a/watson/watson.py
+++ b/watson/watson.py
@@ -101,7 +101,7 @@ def _format_date(self, date):
         if not isinstance(date, arrow.Arrow):
             date = arrow.get(date)
 
-        return date.timestamp
+        return date.int_timestamp
 
     @property
     def config(self):


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index bb247075..a907d260 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -172,14 +172,14 @@ def test_report_invalid_date(runner, watson, test_dt):
 
 @pytest.mark.parametrize('at_dt', VALID_TIMES_DATA)
 def test_stop_valid_time(runner, watson, mocker, at_dt):
-    mocker.patch('arrow.arrow.datetime', wraps=datetime)
+    mocker.patch('arrow.arrow.dt_datetime', wraps=datetime)
     start_dt = datetime(2019, 4, 10, 14, 0, 0, tzinfo=tzlocal())
-    arrow.arrow.datetime.now.return_value = start_dt
+    arrow.arrow.dt_datetime.now.return_value = start_dt
     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.datetime.now.return_value = (start_dt + timedelta(hours=1))
+    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
 
@@ -189,8 +189,8 @@ def test_stop_valid_time(runner, watson, mocker, at_dt):
 @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.datetime', wraps=datetime)
+    mocker.patch('arrow.arrow.dt_datetime', wraps=datetime)
     start_dt = datetime(2019, 4, 10, 14, 0, 0, tzinfo=tzlocal())
-    arrow.arrow.datetime.now.return_value = (start_dt + timedelta(hours=1))
+    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
diff --git a/tests/test_watson.py b/tests/test_watson.py
index c70a1522..14e3b38c 100644
--- a/tests/test_watson.py
+++ b/tests/test_watson.py
@@ -76,7 +76,7 @@ def test_current_with_empty_given_state(config_dir, mocker):
 
 def test_last_sync(mocker, watson):
     now = arrow.get(4123)
-    content = json.dumps(now.timestamp)
+    content = json.dumps(now.int_timestamp)
 
     mocker.patch('builtins.open', mocker.mock_open(read_data=content))
     assert watson.last_sync == now
@@ -482,7 +482,7 @@ def test_save_last_sync(mocker, watson, json_mock):
     watson.save()
 
     assert json_mock.call_count == 1
-    assert json_mock.call_args[0][0] == now.timestamp
+    assert json_mock.call_args[0][0] == now.int_timestamp
 
 
 def test_save_empty_last_sync(config_dir, mocker, json_mock):
@@ -682,14 +682,14 @@ def json(self):
 
     assert watson.frames[0].id == '1c006c6e6cc14c80ab22b51c857c0b06'
     assert watson.frames[0].project == 'foo'
-    assert watson.frames[0].start.timestamp == 4003
-    assert watson.frames[0].stop.timestamp == 4004
+    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[1].id == 'c44aa8154d774a58bddd1afa95562141'
     assert watson.frames[1].project == 'bar'
-    assert watson.frames[1].start.timestamp == 4004
-    assert watson.frames[1].stop.timestamp == 4005
+    assert watson.frames[1].start.int_timestamp == 4004
+    assert watson.frames[1].stop.int_timestamp == 4005
     assert watson.frames[1].tags == []
 
 
@@ -888,18 +888,18 @@ def test_rename_project_with_time(watson):
 
     assert watson.frames[0].id == 'c76d1ad0282c429595cc566d7098c165'
     assert watson.frames[0].project == 'baz'
-    assert watson.frames[0].start.timestamp == 4001
-    assert watson.frames[0].stop.timestamp == 4002
+    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].updated_at.timestamp == 9000
-    assert watson.frames[0].updated_at.timestamp > 4005
+    # 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].start.timestamp == 4010
-    assert watson.frames[1].stop.timestamp == 4015
+    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].updated_at.timestamp == 4035
+    assert watson.frames[1].updated_at.int_timestamp == 4035
 
 
 def test_rename_tag_with_time(watson):
@@ -922,18 +922,18 @@ def test_rename_tag_with_time(watson):
 
     assert watson.frames[0].id == 'c76d1ad0282c429595cc566d7098c165'
     assert watson.frames[0].project == 'foo'
-    assert watson.frames[0].start.timestamp == 4001
-    assert watson.frames[0].stop.timestamp == 4002
+    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].updated_at.timestamp == 4005
+    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].start.timestamp == 4010
-    assert watson.frames[1].stop.timestamp == 4015
+    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].updated_at.timestamp == 9000
-    assert watson.frames[1].updated_at.timestamp > 4035
+    # assert watson.frames[1].updated_at.int_timestamp == 9000
+    assert watson.frames[1].updated_at.int_timestamp > 4035
 
 # add