Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: "3.11"
python-version: "3.13"
cache: 'pip'
- name: Install dependencies
run: sudo apt-get install -y pandoc
Expand Down
26 changes: 12 additions & 14 deletions app/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
import asyncio
import datetime
import logging
import os
from datetime import timedelta
from pathlib import Path

from xdg import xdg_cache_home, xdg_data_home

Expand All @@ -29,13 +29,13 @@
def main():
args = parse_args()

config_dir = f"{xdg_data_home()}/gtasks-md/{args.user}/"
os.makedirs(os.path.dirname(config_dir), exist_ok=True)
cache_dir = f"{xdg_cache_home()}/gtasks-md/{args.user}/"
os.makedirs(os.path.dirname(cache_dir), exist_ok=True)
config_dir = Path(xdg_data_home()) / "gtasks-md" / args.user
config_dir.mkdir(parents=True, exist_ok=True)
cache_dir = Path(xdg_cache_home()) / "gtasks-md" / args.user
cache_dir.mkdir(parents=True, exist_ok=True)

logging.basicConfig(
filename=f"{xdg_cache_home()}/gtasks-md/log.txt",
filename=Path(xdg_cache_home()) / "gtasks-md" / "log.txt",
format="%(asctime)s %(levelname)-8s %(message)s",
encoding="utf-8",
level=logging.INFO,
Expand Down Expand Up @@ -137,8 +137,7 @@ def parse_date(date):


def auth(service: GoogleApiService, file: str):
with open(file, "r") as src_file:
service.save_credentials(src_file.read())
service.save_credentials(Path(file).read_text(encoding="utf-8"))


def view(service: GoogleApiService):
Expand All @@ -157,12 +156,11 @@ def edit(service: GoogleApiService, editor: Editor, backup: Backup):
def reconcile(service: GoogleApiService, file_path: str, backup: Backup | None = None):
old_task_lists, old_text = fetch_task_lists(service)

with open(file_path, "r") as source:
new_text = source.read()
new_task_lists = markdown_to_task_lists(new_text)
if backup:
backup.write_backup(old_text)
asyncio.run(service.reconcile(old_task_lists, new_task_lists))
new_text = Path(file_path).read_text(encoding="utf-8")
new_task_lists = markdown_to_task_lists(new_text)
if backup:
backup.write_backup(old_text)
asyncio.run(service.reconcile(old_task_lists, new_task_lists))


def rollback(service: GoogleApiService, backup: Backup):
Expand Down
19 changes: 9 additions & 10 deletions app/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,31 @@ def __init__(self, user):
self.user = user

def write_backup(self, text: str):
cache_dir = f"{xdg_cache_home()}/gtasks-md/{self.user}"
marker_file_path = Path(f"{cache_dir}/marker")
cache_dir = Path(xdg_cache_home()) / "gtasks-md" / self.user
marker_file_path = cache_dir / "marker"

marker_file_path.touch()
with marker_file_path.open("r+") as marker_file:
with marker_file_path.open("r+", encoding="utf-8") as marker_file:
marker = marker_file.read()
file_no = (int(marker) + 1 if marker else 0) % 10
marker_file.seek(0)
marker_file.write(str(file_no))
marker_file.truncate()

with open(f"{cache_dir}/{file_no}.bak.md", "w") as backup_file:
backup_file.write(text)
(cache_dir / f"{file_no}.bak.md").write_text(text, encoding="utf-8")

def discard_backup(self):
cache_dir = f"{xdg_cache_home()}/gtasks-md/{self.user}"
marker_file_path = Path(f"{cache_dir}/marker")
cache_dir = Path(xdg_cache_home()) / "gtasks-md" / self.user
marker_file_path = cache_dir / "marker"

if not marker_file_path.is_file():
return None

with marker_file_path.open("r+") as marker_file:
with marker_file_path.open("r+", encoding="utf-8") as marker_file:
marker = marker_file.read()
file_no = ((int(marker) if marker else 0)) % 10
file_no = (int(marker) if marker else 0) % 10
marker_file.seek(0)
marker_file.write(str(file_no - 1))
marker_file.truncate()

return f"{cache_dir}/{file_no}.bak.md"
return str(cache_dir / f"{file_no}.bak.md")
19 changes: 9 additions & 10 deletions app/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import os
import subprocess
import tempfile
from pathlib import Path


class Editor:
Expand Down Expand Up @@ -41,17 +42,15 @@ def edit(self, text: str) -> str:
program will try the following options: $VISUAL, $EDITOR and "vim".
"""

tmp_file = ""
with tempfile.NamedTemporaryFile(suffix=".md", delete=False) as tmp:
tmp.write(str.encode(text))
with tempfile.NamedTemporaryFile(
suffix=".md",
mode="w",
encoding="utf-8",
delete_on_close=False,
) as tmp:
tmp.write(text)
tmp.flush()
if subprocess.call([self.editor, tmp.name]) != 0:
exit(1)
tmp_file = tmp.name

out = ""
with open(tmp_file, "r") as output:
out = output.read()

os.remove(tmp_file)
return out
return Path(tmp.name).read_text(encoding="utf-8")
30 changes: 14 additions & 16 deletions app/googleapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
# limitations under the License.
import asyncio
import logging
import os
from collections import defaultdict
from datetime import datetime
from enum import Enum, auto
from pathlib import Path

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
Expand Down Expand Up @@ -243,9 +243,9 @@ def fix_task_order(task_list_id, new_tasks, parent_task_id=""):
f" (parent: {parent_task_id})"
)

async_tasks = []
for op in gen_tasklist_ops():
async_tasks.append(asyncio.create_task(apply_task_list_op(op)))
async_tasks = [
asyncio.create_task(apply_task_list_op(op)) for op in gen_tasklist_ops()
]
await asyncio.gather(*async_tasks)

def fetch_task_lists(self) -> list[TaskList]:
Expand Down Expand Up @@ -352,36 +352,34 @@ def get_credentials(self) -> Credentials:
the process will simply fail.
"""
creds = None
config_dir = f"{xdg_data_home()}/gtasks-md/{self.user}"
credentials_file = f"{config_dir}/{CREDENTIALS_FILE}"
cache_dir = f"{xdg_cache_home()}/gtasks-md/{self.user}"
token_file = f"{cache_dir}/token.json"
config_dir = Path(xdg_data_home()) / "gtasks-md" / self.user
credentials_file = config_dir / CREDENTIALS_FILE
cache_dir = Path(xdg_cache_home()) / "gtasks-md" / self.user
token_file = cache_dir / "token.json"
# The file token.json stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists(token_file):
creds = Credentials.from_authorized_user_file(token_file, SCOPES)
if token_file.exists():
creds = Credentials.from_authorized_user_file(str(token_file), SCOPES)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
credentials_file, SCOPES
str(credentials_file), SCOPES
)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open(token_file, "w+") as token:
token.write(creds.to_json())
token_file.write_text(creds.to_json(), encoding="utf-8")

return creds

def save_credentials(self, credentials: str):
"""Save credentials to selected user config directory."""
config_dir = f"{xdg_data_home()}/gtasks-md/{self.user}"
config_dir = Path(xdg_data_home()) / "gtasks-md" / self.user

with open(f"{config_dir}/{CREDENTIALS_FILE}", "w+") as dest_file:
dest_file.write(credentials)
(config_dir / CREDENTIALS_FILE).write_text(credentials, encoding="utf-8")

def _get_service(self):
if not self._service:
Expand Down
36 changes: 15 additions & 21 deletions app/pandoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,18 @@ def task_lists_to_markdown(task_lists: list[TaskList]) -> str:
def text_to_pandoc(text: str):
elems = []
for word in text.split():
elems.append(Str(word))
elems.append(Space())
elems.extend((Str(word), Space()))
return elems[:-1]

def tasks_to_pandoc(tasks: list[Task]):
pandoc_tasks = []
has_notes = any(t.note for t in tasks)
for task in tasks:
pandoc_tasks.append(task_to_pandoc(task, has_notes))
return pandoc_tasks
return [task_to_pandoc(task, has_notes) for task in tasks]

def task_to_pandoc(task: Task, parent_contains_notes: bool):
pandoc_task = []

task_sign = "☒" if task.completed() else "☐"
task_title = [Str(task_sign), Space()] + text_to_pandoc(task.title)
task_title = [Str(task_sign), Space(), *text_to_pandoc(task.title)]

if parent_contains_notes:
pandoc_task.append(Para(task_title))
Expand All @@ -69,10 +65,11 @@ def task_to_pandoc(task: Task, parent_contains_notes: bool):
pandoc_task.append(Plain(task_title))

if task.subtasks:
subtasks = []
parent_contains_notes = any(st.note for st in task.subtasks)
for subtask in task.subtasks:
subtasks.append(task_to_pandoc(subtask, parent_contains_notes))
subtasks = [
task_to_pandoc(subtask, parent_contains_notes)
for subtask in task.subtasks
]

pandoc_task.append(OrderedList(ORDERED_FIRST_ELEM, subtasks))

Expand All @@ -83,9 +80,11 @@ def task_to_pandoc(task: Task, parent_contains_notes: bool):
]

for task_list in task_lists:
content.append(Header(2, EMPTY_ATTRS, text_to_pandoc(task_list.title)))
content.append(
OrderedList(ORDERED_FIRST_ELEM, tasks_to_pandoc(task_list.tasks))
content.extend(
(
Header(2, EMPTY_ATTRS, text_to_pandoc(task_list.title)),
OrderedList(ORDERED_FIRST_ELEM, tasks_to_pandoc(task_list.tasks)),
)
)

return pandoc.write(Pandoc(Meta({}), content))
Expand All @@ -108,21 +107,16 @@ def parse_task_lists(items, idx):
match items[idx + 1]:
case OrderedList(_, tasks):
task_list.tasks = parse_tasks(tasks)
return [task_list] + parse_task_lists(items, idx + 2)
return [task_list, *parse_task_lists(items, idx + 2)]
case _:
return [task_list] + parse_task_lists(items, idx + 1)
return [task_list, *parse_task_lists(items, idx + 1)]
else:
return [task_list]
case _:
raise SyntaxError(f"Unexpected item while parsing: {items[idx]}")

def parse_tasks(tasks):
parsed_tasks = []

for i, task in enumerate(tasks):
parsed_tasks.append(parse_task(task, i))

return parsed_tasks
return [parse_task(task, i) for i, task in enumerate(tasks)]

def parse_task(task, task_no):
def match_status(str: Str) -> TaskStatus:
Expand Down
9 changes: 2 additions & 7 deletions app/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ def __eq__(self, other: Task) -> bool:
self.title == other.title
and self.note == other.note
and self.status == other.status
and len(self.subtasks) == len(other.subtasks)
and not any(st != ot for (st, ot) in zip(self.subtasks, other.subtasks))
and self.subtasks == other.subtasks
)

def __str__(self) -> str:
Expand Down Expand Up @@ -80,11 +79,7 @@ class TaskList:
tasks: list[Task]

def __eq__(self, other: TaskList) -> bool:
return (
self.title == other.title
and len(self.tasks) == len(other.tasks)
and not any(st != ot for (st, ot) in zip(self.tasks, other.tasks))
)
return self.title == other.title and self.tasks == other.tasks

def __str__(self) -> str:
return f"{self.title} ({self.id}): {len(self.tasks)} tasks"
Expand Down
14 changes: 7 additions & 7 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
inputs = {
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:NixOS/nixpkgs/release-25.11";
nixpkgs.url = "github:NixOS/nixpkgs/release-26.05";
pyproject-nix = {
url = "github:pyproject-nix/pyproject.nix";
inputs.nixpkgs.follows = "nixpkgs";
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ version = "0.0.10"
description = "A tool to manage Google Tasks using a markdown document."
readme = "README.md"
license = "Apache-2.0"
requires-python = ">=3.11"
requires-python = ">=3.13"
authors = [
{ name = "Michal Kielbowicz", email = "gtasks-md@kielbowi.cz" },
]
Expand Down
Loading