Compare commits
1 commit
main
...
feat/add-m
Author | SHA1 | Date | |
---|---|---|---|
544e30dc4c |
11 changed files with 109 additions and 42 deletions
|
@ -7,6 +7,10 @@ and this project tries to adhere to [Semantic Versioning](https://semver.org/spe
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* Compatible with Python stretching back to version 3.7
|
||||||
|
|
||||||
## [0.4] - 2021-12-06
|
## [0.4] - 2021-12-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
16
README.md
16
README.md
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Can take an export of nomie habits in json format and convert it to be importable in Loop Habit Tracker.
|
Can take an export of nomie habits in json format and convert it to be importable in Loop Habit Tracker.
|
||||||
|
|
||||||
Confirmed working for nomie version 5.6.4 and Loop Habit Tracker version 2.0.2.
|
Confirmed working for nomie version 5.6.4 and Loop Habit Tracker version 2.0.2 and 2.0.3.
|
||||||
Presumably works for other nomie 5.x versions and other Loop 2.x versions as well,
|
Presumably works for other nomie 5.x versions and other Loop 2.x versions as well,
|
||||||
but that is untested.
|
but that is untested.
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@ Can also take an existing Loop Habit database (exported from the application),
|
||||||
and add the nomie exported habits and checkmarks to it.
|
and add the nomie exported habits and checkmarks to it.
|
||||||
Simply put the exported Loop database in the same directory and call it `output.db`,
|
Simply put the exported Loop database in the same directory and call it `output.db`,
|
||||||
it will not (should not™️) overwrite anything.
|
it will not (should not™️) overwrite anything.
|
||||||
|
If there are any duplicated habits however,
|
||||||
|
it will add duplications of the existing repetitions into the database.
|
||||||
|
|
||||||
Invoked like: `python run.py nomie-export.json`.
|
Invoked like: `python run.py nomie-export.json`.
|
||||||
Note, however, that -- until a packaged version is released -- you will need to have some packages in your environment.
|
Note, however, that -- until a packaged version is released -- you will need to have some packages in your environment.
|
||||||
|
@ -30,15 +32,3 @@ In the future there might be an easier road to using this package but that's the
|
||||||
The package can also be used as a library to load nomie data
|
The package can also be used as a library to load nomie data
|
||||||
or move data into Loop Habit Tracker.
|
or move data into Loop Habit Tracker.
|
||||||
|
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
* [ ] clean up README
|
|
||||||
* [ ] begin adding tests
|
|
||||||
* [ ] add some unit tests for various functions
|
|
||||||
* [ ] and at least an integration test for the stable database (loop-2021-12-02.db or equivalent)
|
|
||||||
* [ ] abstract migration target away from loop
|
|
||||||
* [ ] abstract import away from nomie
|
|
||||||
* [ ] abstract importer/migrator themselves to work with other targets
|
|
||||||
* [ ] allow migration to/from nomie/loop
|
|
||||||
|
|
||||||
|
|
9
noxfile.py
Normal file
9
noxfile.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import nox
|
||||||
|
|
||||||
|
|
||||||
|
@nox.session(python=["3.7", "3.8", "3.9"])
|
||||||
|
def tests(session):
|
||||||
|
args = session.posargs or ["--cov"]
|
||||||
|
session.run("poetry", "install", external=True)
|
||||||
|
session.run("pytest", *args)
|
||||||
|
pass
|
68
poetry.lock
generated
68
poetry.lock
generated
|
@ -30,6 +30,7 @@ python-versions = ">=3.6"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
|
@ -53,6 +54,21 @@ tomli = {version = "*", optional = true, markers = "extra == \"toml\""}
|
||||||
[package.extras]
|
[package.extras]
|
||||||
toml = ["tomli"]
|
toml = ["tomli"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "importlib-metadata"
|
||||||
|
version = "0.23"
|
||||||
|
description = "Read metadata from Python packages"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
zipp = ">=0.5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx", "rst.linker"]
|
||||||
|
testing = ["packaging", "importlib-resources"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
@ -80,6 +96,9 @@ category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
dev = ["pre-commit", "tox"]
|
dev = ["pre-commit", "tox"]
|
||||||
testing = ["pytest", "pytest-benchmark"]
|
testing = ["pytest", "pytest-benchmark"]
|
||||||
|
@ -115,6 +134,7 @@ python-versions = ">=3.6"
|
||||||
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
|
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
|
||||||
attrs = ">=19.2.0"
|
attrs = ">=19.2.0"
|
||||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
|
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||||
iniconfig = "*"
|
iniconfig = "*"
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
pluggy = ">=0.12,<2.0"
|
pluggy = ">=0.12,<2.0"
|
||||||
|
@ -139,6 +159,20 @@ pytest = ">=4.6"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
|
testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-mock"
|
||||||
|
version = "3.6.1"
|
||||||
|
description = "Thin-wrapper around the mock package for easier use with pytest"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pytest = ">=5.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["pre-commit", "tox", "pytest-asyncio"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
|
@ -149,16 +183,28 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tomli"
|
name = "tomli"
|
||||||
version = "1.2.2"
|
version = "2.0.0"
|
||||||
description = "A lil' TOML parser"
|
description = "A lil' TOML parser"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zipp"
|
||||||
|
version = "3.6.0"
|
||||||
|
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
|
||||||
|
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.9"
|
python-versions = "^3.7"
|
||||||
content-hash = "47104052627c5737e341ecfb58eb57c259f5baf5c99d7251dc860b331e18bee0"
|
content-hash = "fec236ab912efe582781c62e4cd2c4cd7e609557e284e067fa875f30ab806d93"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
atomicwrites = [
|
atomicwrites = [
|
||||||
|
@ -226,6 +272,10 @@ coverage = [
|
||||||
{file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"},
|
{file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"},
|
||||||
{file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"},
|
{file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"},
|
||||||
]
|
]
|
||||||
|
importlib-metadata = [
|
||||||
|
{file = "importlib_metadata-0.23-py2.py3-none-any.whl", hash = "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"},
|
||||||
|
{file = "importlib_metadata-0.23.tar.gz", hash = "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26"},
|
||||||
|
]
|
||||||
iniconfig = [
|
iniconfig = [
|
||||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||||
|
@ -254,11 +304,19 @@ pytest-cov = [
|
||||||
{file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
|
{file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
|
||||||
{file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
|
{file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
|
||||||
]
|
]
|
||||||
|
pytest-mock = [
|
||||||
|
{file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"},
|
||||||
|
{file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"},
|
||||||
|
]
|
||||||
toml = [
|
toml = [
|
||||||
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||||
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||||
]
|
]
|
||||||
tomli = [
|
tomli = [
|
||||||
{file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"},
|
{file = "tomli-2.0.0-py3-none-any.whl", hash = "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224"},
|
||||||
{file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"},
|
{file = "tomli-2.0.0.tar.gz", hash = "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"},
|
||||||
|
]
|
||||||
|
zipp = [
|
||||||
|
{file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"},
|
||||||
|
{file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"},
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,14 +8,15 @@ packages = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
importlib-metadata = {version = "^1.0", python = "<3.8"}
|
importlib-metadata = {version = "^0.23", python = "<3.8"}
|
||||||
python = "^3.9"
|
python = "^3.7"
|
||||||
click = "^8.0"
|
click = "^8.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pytest = "^6.2"
|
pytest = "^6.2"
|
||||||
coverage = {extras = ["toml"], version = "^6.2"}
|
coverage = {extras = ["toml"], version = "^6.2"}
|
||||||
pytest-cov = "^3.0.0"
|
pytest-cov = "^3.0.0"
|
||||||
|
pytest-mock = "^3.6.1"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
habitmove = "habitmove.cli:main"
|
habitmove = "habitmove.cli:main"
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
# init.py
|
# init.py
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
try:
|
||||||
from importlib.metadata import version as metadata_version
|
from importlib.metadata import version as metadata_version
|
||||||
else:
|
except ImportError:
|
||||||
from importlib_metadata import version as metadata_version
|
from importlib_metadata import version as metadata_version
|
||||||
|
|
||||||
__version__ = str(metadata_version(__name__))
|
__version__ = str(metadata_version(__name__))
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
from habitmove.nomiedata import Tracker
|
from habitmove.nomiedata import Tracker
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Union
|
from typing import Any, Union
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
import re
|
import re
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
@ -92,7 +94,8 @@ def add_to_database(
|
||||||
(sql_id, repetition.timestamp, repetition.value),
|
(sql_id, repetition.timestamp, repetition.value),
|
||||||
)
|
)
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
# TODO better error handling
|
# FIXME better error handling
|
||||||
|
# TODO think about adapting this to allow importing into existing databases
|
||||||
print(
|
print(
|
||||||
f"{sql_id}, {habit.name}: timestamp {datetime.fromtimestamp(repetition.timestamp/1000)} not unique, moving timestamp slightly."
|
f"{sql_id}, {habit.name}: timestamp {datetime.fromtimestamp(repetition.timestamp/1000)} not unique, moving timestamp slightly."
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
def create_database(name):
|
def create_database(db_file: str = ":memory:") -> sqlite3.Connection:
|
||||||
"""create a database connection to the SQLite database
|
"""create a database connection to the SQLite database
|
||||||
specified by db_file
|
specified by db_file
|
||||||
:param db_file: database file
|
:param db_file: database file
|
||||||
|
@ -9,16 +10,14 @@ def create_database(name):
|
||||||
"""
|
"""
|
||||||
conn = None
|
conn = None
|
||||||
try:
|
try:
|
||||||
conn = sqlite3.connect(name)
|
conn = sqlite3.connect(db_file)
|
||||||
return conn
|
return conn
|
||||||
except sqlite3.Error as e:
|
except sqlite3.Error as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
sys.exit(1)
|
||||||
return conn
|
|
||||||
|
|
||||||
|
|
||||||
def create_tables(db):
|
def create_tables(c: sqlite3.Cursor):
|
||||||
c = db.cursor()
|
|
||||||
c.execute(
|
c.execute(
|
||||||
""" CREATE TABLE IF NOT EXISTS Habits (
|
""" CREATE TABLE IF NOT EXISTS Habits (
|
||||||
id integer PRIMARY KEY AUTOINCREMENT,
|
id integer PRIMARY KEY AUTOINCREMENT,
|
||||||
|
@ -51,8 +50,7 @@ def create_tables(db):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_constraints(db):
|
def create_constraints(c: sqlite3.Cursor):
|
||||||
c = db.cursor()
|
|
||||||
c.execute(
|
c.execute(
|
||||||
""" CREATE UNIQUE INDEX IF NOT EXISTS idx_repetitions_habit_timestamp
|
""" CREATE UNIQUE INDEX IF NOT EXISTS idx_repetitions_habit_timestamp
|
||||||
on Repetitions( habit, timestamp);
|
on Repetitions( habit, timestamp);
|
||||||
|
@ -60,15 +58,15 @@ def create_constraints(db):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_pragma(db):
|
def create_pragma(c: sqlite3.Cursor):
|
||||||
c = db.cursor()
|
|
||||||
c.execute(""" PRAGMA user_version = 24; """)
|
c.execute(""" PRAGMA user_version = 24; """)
|
||||||
c.execute(""" PRAGMA schema_version = 30; """)
|
c.execute(""" PRAGMA schema_version = 30; """)
|
||||||
|
|
||||||
|
|
||||||
def migrate(name):
|
def migrate(fname):
|
||||||
db = create_database(name)
|
db = create_database(fname)
|
||||||
create_tables(db)
|
c = db.cursor()
|
||||||
create_constraints(db)
|
create_tables(c)
|
||||||
create_pragma(db)
|
create_constraints(c)
|
||||||
|
create_pragma(c)
|
||||||
return db
|
return db
|
||||||
|
|
|
@ -37,8 +37,8 @@ def trackerlist():
|
||||||
|
|
||||||
|
|
||||||
def test_simple_habit_transform_from_tracker(trackerlist):
|
def test_simple_habit_transform_from_tracker(trackerlist):
|
||||||
sut = trackerlist[0]
|
result = habits.trackers_to_habits([trackerlist[0]])
|
||||||
assert habits.trackers_to_habits([sut]) == [
|
assert result == [
|
||||||
loopdata.Habit(
|
loopdata.Habit(
|
||||||
name="🧪 Testing",
|
name="🧪 Testing",
|
||||||
description="testtrack",
|
description="testtrack",
|
||||||
|
@ -51,8 +51,8 @@ def test_simple_habit_transform_from_tracker(trackerlist):
|
||||||
|
|
||||||
|
|
||||||
def test_range_habit_transform_from_tracker(trackerlist):
|
def test_range_habit_transform_from_tracker(trackerlist):
|
||||||
sut = trackerlist[1]
|
result = habits.trackers_to_habits([trackerlist[1]])
|
||||||
assert habits.trackers_to_habits([sut]) == [
|
assert result == [
|
||||||
loopdata.Habit(
|
loopdata.Habit(
|
||||||
name="🧪 Testing",
|
name="🧪 Testing",
|
||||||
description="testtrack",
|
description="testtrack",
|
||||||
|
|
Loading…
Reference in a new issue